diff --git a/auth/build.gradle b/auth/build.gradle
index 30cd3226f5..14af19ec57 100644
--- a/auth/build.gradle
+++ b/auth/build.gradle
@@ -5,7 +5,8 @@ plugins {
description = "gRpc: Auth"
dependencies {
compile project(':grpc-core'),
- libraries.oauth_client
+ libraries.google_auth_credentials
+ testCompile libraries.oauth_client
}
// Configure the animal sniffer plugin
diff --git a/auth/src/main/java/io/grpc/auth/ClientAuthInterceptor.java b/auth/src/main/java/io/grpc/auth/ClientAuthInterceptor.java
index e636b3996b..aa2df86f88 100644
--- a/auth/src/main/java/io/grpc/auth/ClientAuthInterceptor.java
+++ b/auth/src/main/java/io/grpc/auth/ClientAuthInterceptor.java
@@ -38,17 +38,8 @@ import io.grpc.CallOptions;
import io.grpc.Channel;
import io.grpc.ClientCall;
import io.grpc.ClientInterceptor;
-import io.grpc.ClientInterceptors.CheckedForwardingClientCall;
-import io.grpc.Metadata;
import io.grpc.MethodDescriptor;
-import io.grpc.Status;
-import io.grpc.StatusException;
-import java.io.IOException;
-import java.net.URI;
-import java.net.URISyntaxException;
-import java.util.List;
-import java.util.Map;
import java.util.concurrent.Executor;
/**
@@ -57,109 +48,22 @@ import java.util.concurrent.Executor;
*
*
Uses the new and simplified Google auth library:
* https://github.com/google/google-auth-library-java
+ *
+ * @deprecated use {@link GoogleAuthLibraryCallCredentials} instead.
*/
+@Deprecated
public final class ClientAuthInterceptor implements ClientInterceptor {
private final Credentials credentials;
- private Metadata cached;
- private Map> lastMetadata;
-
public ClientAuthInterceptor(
Credentials credentials, @SuppressWarnings("unused") Executor executor) {
this.credentials = Preconditions.checkNotNull(credentials);
- // TODO(louiscryan): refresh token asynchronously with this executor.
}
@Override
public ClientCall interceptCall(
final MethodDescriptor method, CallOptions callOptions, final Channel next) {
- // TODO(ejona86): If the call fails for Auth reasons, this does not properly propagate info that
- // would be in WWW-Authenticate, because it does not yet have access to the header.
- return new CheckedForwardingClientCall(next.newCall(method, callOptions)) {
- @Override
- protected void checkedStart(Listener responseListener, Metadata headers)
- throws StatusException {
- Metadata cachedSaved;
- URI uri = serviceUri(next, method);
- synchronized (ClientAuthInterceptor.this) {
- // TODO(louiscryan): This is icky but the current auth library stores the same
- // metadata map until the next refresh cycle. This will be fixed once
- // https://github.com/google/google-auth-library-java/issues/3
- // is resolved.
- // getRequestMetadata() may return a different map based on the provided URI, i.e., for
- // JWT. However, today it does not cache JWT and so we won't bother tring to cache its
- // return value based on the URI.
- Map> latestMetadata = getRequestMetadata(uri);
- if (lastMetadata == null || lastMetadata != latestMetadata) {
- lastMetadata = latestMetadata;
- cached = toHeaders(lastMetadata);
- }
- cachedSaved = cached;
- }
- headers.merge(cachedSaved);
- delegate().start(responseListener, headers);
- }
- };
- }
-
- /**
- * Generate a JWT-specific service URI. The URI is simply an identifier with enough information
- * for a service to know that the JWT was intended for it. The URI will commonly be verified with
- * a simple string equality check.
- */
- private URI serviceUri(Channel channel, MethodDescriptor, ?> method) throws StatusException {
- String authority = channel.authority();
- if (authority == null) {
- throw Status.UNAUTHENTICATED.withDescription("Channel has no authority").asException();
- }
- // Always use HTTPS, by definition.
- final String scheme = "https";
- final int defaultPort = 443;
- String path = "/" + MethodDescriptor.extractFullServiceName(method.getFullMethodName());
- URI uri;
- try {
- uri = new URI(scheme, authority, path, null, null);
- } catch (URISyntaxException e) {
- throw Status.UNAUTHENTICATED.withDescription("Unable to construct service URI for auth")
- .withCause(e).asException();
- }
- // The default port must not be present. Alternative ports should be present.
- if (uri.getPort() == defaultPort) {
- uri = removePort(uri);
- }
- return uri;
- }
-
- private URI removePort(URI uri) throws StatusException {
- try {
- return new URI(uri.getScheme(), uri.getUserInfo(), uri.getHost(), -1 /* port */,
- uri.getPath(), uri.getQuery(), uri.getFragment());
- } catch (URISyntaxException e) {
- throw Status.UNAUTHENTICATED.withDescription(
- "Unable to construct service URI after removing port")
- .withCause(e).asException();
- }
- }
-
- private Map> getRequestMetadata(URI uri) throws StatusException {
- try {
- return credentials.getRequestMetadata(uri);
- } catch (IOException e) {
- throw Status.UNAUTHENTICATED.withCause(e).asException();
- }
- }
-
- private static final Metadata toHeaders(Map> metadata) {
- Metadata headers = new Metadata();
- if (metadata != null) {
- for (String key : metadata.keySet()) {
- Metadata.Key headerKey = Metadata.Key.of(key, Metadata.ASCII_STRING_MARSHALLER);
- for (String value : metadata.get(key)) {
- headers.put(headerKey, value);
- }
- }
- }
- return headers;
+ return next.newCall(method, callOptions.withCredentials(MoreCallCredentials.from(credentials)));
}
}
diff --git a/auth/src/main/java/io/grpc/auth/GoogleAuthLibraryCallCredentials.java b/auth/src/main/java/io/grpc/auth/GoogleAuthLibraryCallCredentials.java
new file mode 100644
index 0000000000..e5b5cd7316
--- /dev/null
+++ b/auth/src/main/java/io/grpc/auth/GoogleAuthLibraryCallCredentials.java
@@ -0,0 +1,168 @@
+/*
+ * Copyright 2016, Google Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * * Neither the name of Google Inc. nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package io.grpc.auth;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.auth.Credentials;
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.io.BaseEncoding;
+
+import io.grpc.Attributes;
+import io.grpc.CallCredentials.MetadataApplier;
+import io.grpc.CallCredentials;
+import io.grpc.Metadata;
+import io.grpc.MethodDescriptor;
+import io.grpc.Status;
+import io.grpc.StatusException;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.Executor;
+
+/**
+ * Wraps {@link Credentials} as a {@link CallCredentials}.
+ */
+final class GoogleAuthLibraryCallCredentials implements CallCredentials {
+
+ @VisibleForTesting
+ final Credentials creds;
+
+ private Metadata lastHeaders;
+ private Map> lastMetadata;
+
+ public GoogleAuthLibraryCallCredentials(Credentials creds) {
+ this.creds = checkNotNull(creds, "creds");
+ }
+
+ @Override
+ public void applyRequestMetadata(MethodDescriptor, ?> method, Attributes attrs,
+ Executor appExecutor, final MetadataApplier applier) {
+ Metadata cachedSaved;
+ String authority = checkNotNull(attrs.get(ATTR_AUTHORITY), "authority");
+ final URI uri;
+ try {
+ uri = serviceUri(authority, method);
+ } catch (StatusException e) {
+ applier.fail(e.getStatus());
+ return;
+ }
+ appExecutor.execute(new Runnable() {
+ @Override
+ public void run() {
+ try {
+ // Credentials is expected to manage caching internally if the metadata is fetched over
+ // the network.
+ // TODO(zhangkun83): we don't know whether there is valid cache data. If there is, we
+ // would waste a context switch by always scheduling in executor. However, we have to
+ // do so because we can't risk blocking the network thread. This can be resolved after
+ // https://github.com/google/google-auth-library-java/issues/3 is resolved.
+ Map> metadata = creds.getRequestMetadata(uri);
+ // Re-use the headers if getRequestMetadata() returns the same map. It may return a
+ // different map based on the provided URI, i.e., for JWT. However, today it does not
+ // cache JWT and so we won't bother tring to save its return value based on the URI.
+ Metadata headers;
+ synchronized (GoogleAuthLibraryCallCredentials.this) {
+ if (lastMetadata != metadata) {
+ lastMetadata = metadata;
+ lastHeaders = toHeaders(metadata);
+ }
+ headers = lastHeaders;
+ }
+ applier.apply(headers);
+ } catch (Throwable e) {
+ applier.fail(Status.UNAUTHENTICATED.withCause(e));
+ }
+ }
+ });
+ }
+
+ /**
+ * Generate a JWT-specific service URI. The URI is simply an identifier with enough information
+ * for a service to know that the JWT was intended for it. The URI will commonly be verified with
+ * a simple string equality check.
+ */
+ private static URI serviceUri(String authority, MethodDescriptor, ?> method)
+ throws StatusException {
+ if (authority == null) {
+ throw Status.UNAUTHENTICATED.withDescription("Channel has no authority").asException();
+ }
+ // Always use HTTPS, by definition.
+ final String scheme = "https";
+ final int defaultPort = 443;
+ String path = "/" + MethodDescriptor.extractFullServiceName(method.getFullMethodName());
+ URI uri;
+ try {
+ uri = new URI(scheme, authority, path, null, null);
+ } catch (URISyntaxException e) {
+ throw Status.UNAUTHENTICATED.withDescription("Unable to construct service URI for auth")
+ .withCause(e).asException();
+ }
+ // The default port must not be present. Alternative ports should be present.
+ if (uri.getPort() == defaultPort) {
+ uri = removePort(uri);
+ }
+ return uri;
+ }
+
+ private static URI removePort(URI uri) throws StatusException {
+ try {
+ return new URI(uri.getScheme(), uri.getUserInfo(), uri.getHost(), -1 /* port */,
+ uri.getPath(), uri.getQuery(), uri.getFragment());
+ } catch (URISyntaxException e) {
+ throw Status.UNAUTHENTICATED.withDescription(
+ "Unable to construct service URI after removing port").withCause(e).asException();
+ }
+ }
+
+ private static Metadata toHeaders(Map> metadata) {
+ Metadata headers = new Metadata();
+ if (metadata != null) {
+ for (String key : metadata.keySet()) {
+ if (key.endsWith("-bin")) {
+ Metadata.Key headerKey = Metadata.Key.of(key, Metadata.BINARY_BYTE_MARSHALLER);
+ for (String value : metadata.get(key)) {
+ headers.put(headerKey, BaseEncoding.base64().decode(value));
+ }
+ } else {
+ Metadata.Key headerKey = Metadata.Key.of(key, Metadata.ASCII_STRING_MARSHALLER);
+ for (String value : metadata.get(key)) {
+ headers.put(headerKey, value);
+ }
+ }
+ }
+ }
+ return headers;
+ }
+}
diff --git a/auth/src/main/java/io/grpc/auth/MoreCallCredentials.java b/auth/src/main/java/io/grpc/auth/MoreCallCredentials.java
new file mode 100644
index 0000000000..b8e8ca52a2
--- /dev/null
+++ b/auth/src/main/java/io/grpc/auth/MoreCallCredentials.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2016, Google Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * * Neither the name of Google Inc. nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package io.grpc.auth;
+
+import com.google.auth.Credentials;
+
+import io.grpc.CallCredentials;
+import io.grpc.ExperimentalApi;
+
+/**
+ * A utility class that converts other types of credentials to {@link CallCredentials}.
+ */
+@ExperimentalApi("https//github.com/grpc/grpc-java/issues/1914")
+public final class MoreCallCredentials {
+ /**
+ * Converts a Google Auth Library {@link Credentials} to {@link CallCredentials}.
+ */
+ public static CallCredentials from(Credentials creds) {
+ return new GoogleAuthLibraryCallCredentials(creds);
+ }
+
+ private MoreCallCredentials() {
+ }
+}
diff --git a/auth/src/test/java/io/grpc/auth/ClientAuthInterceptorTests.java b/auth/src/test/java/io/grpc/auth/ClientAuthInterceptorTests.java
index 29fc20100b..ce478a561d 100644
--- a/auth/src/test/java/io/grpc/auth/ClientAuthInterceptorTests.java
+++ b/auth/src/test/java/io/grpc/auth/ClientAuthInterceptorTests.java
@@ -31,58 +31,37 @@
package io.grpc.auth;
-import static org.mockito.Matchers.any;
-import static org.mockito.Matchers.isA;
+import static org.junit.Assert.assertSame;
import static org.mockito.Matchers.same;
-import static org.mockito.Mockito.doReturn;
-import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
import com.google.auth.Credentials;
-import com.google.auth.oauth2.AccessToken;
-import com.google.auth.oauth2.OAuth2Credentials;
-import com.google.common.collect.Iterables;
-import com.google.common.collect.LinkedListMultimap;
-import com.google.common.collect.ListMultimap;
-import com.google.common.collect.Multimaps;
-import com.google.common.util.concurrent.MoreExecutors;
import io.grpc.CallOptions;
import io.grpc.Channel;
import io.grpc.ClientCall;
-import io.grpc.Metadata;
import io.grpc.MethodDescriptor;
import io.grpc.MethodDescriptor.Marshaller;
-import io.grpc.Status;
-import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
import org.mockito.Mock;
-import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
-import java.io.IOException;
-import java.net.URI;
-import java.util.Date;
import java.util.concurrent.Executor;
/**
* Tests for {@link ClientAuthInterceptor}.
*/
@RunWith(JUnit4.class)
+@Deprecated
public class ClientAuthInterceptorTests {
-
- private static final Metadata.Key AUTHORIZATION = Metadata.Key.of("Authorization",
- Metadata.ASCII_STRING_MARSHALLER);
- private static final Metadata.Key EXTRA_AUTHORIZATION = Metadata.Key.of(
- "Extra-Authorization", Metadata.ASCII_STRING_MARSHALLER);
-
- private final Executor executor = MoreExecutors.directExecutor();
+ @Mock
+ Executor executor;
@Mock
Credentials credentials;
@@ -95,14 +74,11 @@ public class ClientAuthInterceptorTests {
MethodDescriptor descriptor;
- @Mock
- ClientCall.Listener listener;
-
@Mock
Channel channel;
- @Mock
- ClientCall call;
+ @Captor
+ ArgumentCaptor callOptionsCaptor;
ClientAuthInterceptor interceptor;
@@ -112,82 +88,17 @@ public class ClientAuthInterceptorTests {
MockitoAnnotations.initMocks(this);
descriptor = MethodDescriptor.create(
MethodDescriptor.MethodType.UNKNOWN, "a.service/method", stringMarshaller, intMarshaller);
- when(channel.newCall(same(descriptor), any(CallOptions.class))).thenReturn(call);
- doReturn("localhost:443").when(channel).authority();
interceptor = new ClientAuthInterceptor(credentials, executor);
}
@Test
- public void testCopyCredentialToHeaders() throws IOException {
- ListMultimap values = LinkedListMultimap.create();
- values.put("Authorization", "token1");
- values.put("Authorization", "token2");
- values.put("Extra-Authorization", "token3");
- values.put("Extra-Authorization", "token4");
- when(credentials.getRequestMetadata(any(URI.class))).thenReturn(Multimaps.asMap(values));
+ public void callCredentialsSet() throws Exception {
ClientCall interceptedCall =
interceptor.interceptCall(descriptor, CallOptions.DEFAULT, channel);
- Metadata headers = new Metadata();
- interceptedCall.start(listener, headers);
- verify(call).start(listener, headers);
- Iterable authorization = headers.getAll(AUTHORIZATION);
- Assert.assertArrayEquals(new String[]{"token1", "token2"},
- Iterables.toArray(authorization, String.class));
- Iterable extraAuthorization = headers.getAll(EXTRA_AUTHORIZATION);
- Assert.assertArrayEquals(new String[]{"token3", "token4"},
- Iterables.toArray(extraAuthorization, String.class));
- }
-
- @Test
- public void testCredentialsThrows() throws IOException {
- when(credentials.getRequestMetadata(any(URI.class))).thenThrow(new IOException("Broken"));
- ClientCall interceptedCall =
- interceptor.interceptCall(descriptor, CallOptions.DEFAULT, channel);
- Metadata headers = new Metadata();
- interceptedCall.start(listener, headers);
- ArgumentCaptor statusCaptor = ArgumentCaptor.forClass(Status.class);
- Mockito.verify(listener).onClose(statusCaptor.capture(), isA(Metadata.class));
- Assert.assertNull(headers.getAll(AUTHORIZATION));
- Mockito.verify(call, never()).start(listener, headers);
- Assert.assertEquals(Status.Code.UNAUTHENTICATED, statusCaptor.getValue().getCode());
- Assert.assertNotNull(statusCaptor.getValue().getCause());
- }
-
- @Test
- public void testWithOAuth2Credential() {
- final AccessToken token = new AccessToken("allyourbase", new Date(Long.MAX_VALUE));
- final OAuth2Credentials oAuth2Credentials = new OAuth2Credentials() {
- @Override
- public AccessToken refreshAccessToken() throws IOException {
- return token;
- }
- };
- interceptor = new ClientAuthInterceptor(oAuth2Credentials, executor);
- ClientCall interceptedCall =
- interceptor.interceptCall(descriptor, CallOptions.DEFAULT, channel);
- Metadata headers = new Metadata();
- interceptedCall.start(listener, headers);
- verify(call).start(listener, headers);
- Iterable authorization = headers.getAll(AUTHORIZATION);
- Assert.assertArrayEquals(new String[]{"Bearer allyourbase"},
- Iterables.toArray(authorization, String.class));
- }
-
- @Test
- public void verifyServiceUri() throws IOException {
- ClientCall interceptedCall;
-
- doReturn("example.com:443").when(channel).authority();
- interceptedCall = interceptor.interceptCall(descriptor, CallOptions.DEFAULT, channel);
- interceptedCall.start(listener, new Metadata());
- verify(credentials).getRequestMetadata(URI.create("https://example.com/a.service"));
- interceptedCall.cancel("Cancel for test", null);
-
- doReturn("example.com:123").when(channel).authority();
- interceptedCall = interceptor.interceptCall(descriptor, CallOptions.DEFAULT, channel);
- interceptedCall.start(listener, new Metadata());
- verify(credentials).getRequestMetadata(URI.create("https://example.com:123/a.service"));
- interceptedCall.cancel("Cancel for test", null);
+ verify(channel).newCall(same(descriptor), callOptionsCaptor.capture());
+ GoogleAuthLibraryCallCredentials callCredentials =
+ (GoogleAuthLibraryCallCredentials) callOptionsCaptor.getValue().getCredentials();
+ assertSame(credentials, callCredentials.creds);
}
}
diff --git a/auth/src/test/java/io/grpc/auth/GoogleAuthLibraryCallCredentialsTests.java b/auth/src/test/java/io/grpc/auth/GoogleAuthLibraryCallCredentialsTests.java
new file mode 100644
index 0000000000..d99812f3db
--- /dev/null
+++ b/auth/src/test/java/io/grpc/auth/GoogleAuthLibraryCallCredentialsTests.java
@@ -0,0 +1,249 @@
+/*
+ * Copyright 2016, Google Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ *
+ * * Neither the name of Google Inc. nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package io.grpc.auth;
+
+import static com.google.common.base.Charsets.US_ASCII;
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.mockito.Matchers.any;
+import static org.mockito.Matchers.eq;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import com.google.auth.Credentials;
+import com.google.auth.oauth2.AccessToken;
+import com.google.auth.oauth2.OAuth2Credentials;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.LinkedListMultimap;
+import com.google.common.collect.ListMultimap;
+import com.google.common.collect.Multimaps;
+
+import io.grpc.Attributes;
+import io.grpc.CallCredentials.MetadataApplier;
+import io.grpc.CallCredentials;
+import io.grpc.Metadata;
+import io.grpc.MethodDescriptor;
+import io.grpc.SecurityLevel;
+import io.grpc.Status;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+
+import java.io.IOException;
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.concurrent.Executor;
+
+/**
+ * Tests for {@link GoogleAuthLibraryCallCredentials}.
+ */
+@RunWith(JUnit4.class)
+public class GoogleAuthLibraryCallCredentialsTests {
+
+ private static final Metadata.Key AUTHORIZATION = Metadata.Key.of("Authorization",
+ Metadata.ASCII_STRING_MARSHALLER);
+ private static final Metadata.Key EXTRA_AUTHORIZATION = Metadata.Key.of(
+ "Extra-Authorization-bin", Metadata.BINARY_BYTE_MARSHALLER);
+
+ @Mock
+ private MethodDescriptor.Marshaller stringMarshaller;
+
+ @Mock
+ private MethodDescriptor.Marshaller intMarshaller;
+
+ @Mock
+ private Credentials credentials;
+
+ @Mock
+ private MetadataApplier applier;
+
+ @Mock
+ private Executor executor;
+
+ @Captor
+ private ArgumentCaptor headersCaptor;
+
+ @Captor
+ private ArgumentCaptor statusCaptor;
+
+ private MethodDescriptor method;
+ private URI expectedUri;
+
+ private final String authority = "testauthority";
+ private final Attributes attrs = Attributes.newBuilder()
+ .set(CallCredentials.ATTR_AUTHORITY, authority)
+ .set(CallCredentials.ATTR_SECURITY_LEVEL, SecurityLevel.PRIVACY_AND_INTEGRITY)
+ .build();
+
+ private ArrayList pendingRunnables = new ArrayList();
+
+ @Before
+ public void setUp() throws Exception {
+ MockitoAnnotations.initMocks(this);
+ method = MethodDescriptor.create(
+ MethodDescriptor.MethodType.UNKNOWN, "a.service/method", stringMarshaller, intMarshaller);
+ expectedUri = new URI("https://testauthority/a.service");
+ doAnswer(new Answer() {
+ @Override
+ public Void answer(InvocationOnMock invocation) {
+ Runnable r = (Runnable) invocation.getArguments()[0];
+ pendingRunnables.add(r);
+ return null;
+ }
+ }).when(executor).execute(any(Runnable.class));
+ }
+
+ @Test
+ public void copyCredentialsToHeaders() throws Exception {
+ ListMultimap values = LinkedListMultimap.create();
+ values.put("Authorization", "token1");
+ values.put("Authorization", "token2");
+ values.put("Extra-Authorization-bin", "dG9rZW4z"); // bytes "token3" in base64
+ values.put("Extra-Authorization-bin", "dG9rZW40"); // bytes "token4" in base64
+ when(credentials.getRequestMetadata(eq(expectedUri))).thenReturn(Multimaps.asMap(values));
+
+ GoogleAuthLibraryCallCredentials callCredentials =
+ new GoogleAuthLibraryCallCredentials(credentials);
+ callCredentials.applyRequestMetadata(method, attrs, executor, applier);
+ assertEquals(1, runPendingRunnables());
+
+ verify(credentials).getRequestMetadata(eq(expectedUri));
+ verify(applier).apply(headersCaptor.capture());
+ Metadata headers = headersCaptor.getValue();
+ Iterable authorization = headers.getAll(AUTHORIZATION);
+ assertArrayEquals(new String[]{"token1", "token2"},
+ Iterables.toArray(authorization, String.class));
+ Iterable extraAuthorization = headers.getAll(EXTRA_AUTHORIZATION);
+ assertEquals(2, Iterables.size(extraAuthorization));
+ assertArrayEquals("token3".getBytes(US_ASCII), Iterables.get(extraAuthorization, 0));
+ assertArrayEquals("token4".getBytes(US_ASCII), Iterables.get(extraAuthorization, 1));
+ }
+
+ @Test
+ public void invalidBase64() throws Exception {
+ ListMultimap values = LinkedListMultimap.create();
+ values.put("Extra-Authorization-bin", "dG9rZW4z1"); // invalid base64
+ when(credentials.getRequestMetadata(eq(expectedUri))).thenReturn(Multimaps.asMap(values));
+
+ GoogleAuthLibraryCallCredentials callCredentials =
+ new GoogleAuthLibraryCallCredentials(credentials);
+ callCredentials.applyRequestMetadata(method, attrs, executor, applier);
+ assertEquals(1, runPendingRunnables());
+
+ verify(credentials).getRequestMetadata(eq(expectedUri));
+ verify(applier).fail(statusCaptor.capture());
+ Status status = statusCaptor.getValue();
+ assertEquals(Status.Code.UNAUTHENTICATED, status.getCode());
+ assertEquals(IllegalArgumentException.class, status.getCause().getClass());
+ }
+
+ @Test
+ public void credentialsThrows() throws Exception {
+ IOException exception = new IOException("Broken");
+ when(credentials.getRequestMetadata(eq(expectedUri))).thenThrow(exception);
+
+ GoogleAuthLibraryCallCredentials callCredentials =
+ new GoogleAuthLibraryCallCredentials(credentials);
+ callCredentials.applyRequestMetadata(method, attrs, executor, applier);
+ assertEquals(1, runPendingRunnables());
+
+ verify(credentials).getRequestMetadata(eq(expectedUri));
+ verify(applier).fail(statusCaptor.capture());
+ Status status = statusCaptor.getValue();
+ assertEquals(Status.Code.UNAUTHENTICATED, status.getCode());
+ assertEquals(exception, status.getCause());
+ }
+
+ @Test
+ public void oauth2Credential() {
+ final AccessToken token = new AccessToken("allyourbase", new Date(Long.MAX_VALUE));
+ final OAuth2Credentials credentials = new OAuth2Credentials() {
+ @Override
+ public AccessToken refreshAccessToken() throws IOException {
+ return token;
+ }
+ };
+
+ GoogleAuthLibraryCallCredentials callCredentials =
+ new GoogleAuthLibraryCallCredentials(credentials);
+ callCredentials.applyRequestMetadata(method, attrs, executor, applier);
+ assertEquals(1, runPendingRunnables());
+
+ verify(applier).apply(headersCaptor.capture());
+ Metadata headers = headersCaptor.getValue();
+ Iterable authorization = headers.getAll(AUTHORIZATION);
+ assertArrayEquals(new String[]{"Bearer allyourbase"},
+ Iterables.toArray(authorization, String.class));
+ }
+
+ @Test
+ public void serviceUri() throws Exception {
+ GoogleAuthLibraryCallCredentials callCredentials =
+ new GoogleAuthLibraryCallCredentials(credentials);
+ callCredentials.applyRequestMetadata(method,
+ Attributes.newBuilder()
+ .setAll(attrs)
+ .set(CallCredentials.ATTR_AUTHORITY, "example.com:443")
+ .build(),
+ executor, applier);
+ assertEquals(1, runPendingRunnables());
+ verify(credentials).getRequestMetadata(eq(new URI("https://example.com/a.service")));
+
+ callCredentials.applyRequestMetadata(method,
+ Attributes.newBuilder()
+ .setAll(attrs)
+ .set(CallCredentials.ATTR_AUTHORITY, "example.com:123")
+ .build(),
+ executor, applier);
+ assertEquals(1, runPendingRunnables());
+ verify(credentials).getRequestMetadata(eq(new URI("https://example.com:123/a.service")));
+ }
+
+ private int runPendingRunnables() {
+ ArrayList savedPendingRunnables = pendingRunnables;
+ pendingRunnables = new ArrayList();
+ for (Runnable r : savedPendingRunnables) {
+ r.run();
+ }
+ return savedPendingRunnables.size();
+ }
+}
diff --git a/build.gradle b/build.gradle
index 6d9e36ec94..62af69b251 100644
--- a/build.gradle
+++ b/build.gradle
@@ -134,7 +134,8 @@ subprojects {
guava: "com.google.guava:guava:${guavaVersion}",
hpack: 'com.twitter:hpack:0.10.1',
jsr305: 'com.google.code.findbugs:jsr305:3.0.0',
- oauth_client: 'com.google.auth:google-auth-library-oauth2-http:0.3.0',
+ oauth_client: 'com.google.auth:google-auth-library-oauth2-http:0.4.0',
+ google_auth_credentials: 'com.google.auth:google-auth-library-credentials:0.4.0',
okhttp: 'com.squareup.okhttp:okhttp:2.5.0',
okio: 'com.squareup.okio:okio:1.6.0',
protobuf: "com.google.protobuf:protobuf-java:${protobufVersion}",
diff --git a/interop-testing/src/main/java/io/grpc/testing/integration/AbstractInteropTest.java b/interop-testing/src/main/java/io/grpc/testing/integration/AbstractInteropTest.java
index b3fc2ca0a7..62b1476eb3 100644
--- a/interop-testing/src/main/java/io/grpc/testing/integration/AbstractInteropTest.java
+++ b/interop-testing/src/main/java/io/grpc/testing/integration/AbstractInteropTest.java
@@ -66,7 +66,7 @@ import io.grpc.ServerInterceptor;
import io.grpc.ServerInterceptors;
import io.grpc.Status;
import io.grpc.StatusRuntimeException;
-import io.grpc.auth.ClientAuthInterceptor;
+import io.grpc.auth.MoreCallCredentials;
import io.grpc.internal.GrpcUtil;
import io.grpc.protobuf.ProtoUtils;
import io.grpc.stub.MetadataUtils;
@@ -802,8 +802,7 @@ public abstract class AbstractInteropTest {
ServiceAccountCredentials.class.cast(GoogleCredentials.fromStream(credentialsStream));
credentials = credentials.createScoped(Arrays.asList(authScope));
TestServiceGrpc.TestServiceBlockingStub stub = blockingStub
- .withInterceptors(new ClientAuthInterceptor(credentials,
- Executors.newSingleThreadExecutor()));
+ .withCallCredentials(MoreCallCredentials.from(credentials));
final SimpleRequest request = SimpleRequest.newBuilder()
.setFillUsername(true)
.setFillOauthScope(true)
@@ -835,8 +834,7 @@ public abstract class AbstractInteropTest {
public void computeEngineCreds(String serviceAccount, String oauthScope) throws Exception {
ComputeEngineCredentials credentials = new ComputeEngineCredentials();
TestServiceGrpc.TestServiceBlockingStub stub = blockingStub
- .withInterceptors(new ClientAuthInterceptor(credentials,
- Executors.newSingleThreadExecutor()));
+ .withCallCredentials(MoreCallCredentials.from(credentials));
final SimpleRequest request = SimpleRequest.newBuilder()
.setFillUsername(true)
.setFillOauthScope(true)
@@ -878,8 +876,7 @@ public abstract class AbstractInteropTest {
origCreds.getClientId(), origCreds.getClientEmail(), origCreds.getPrivateKey(),
origCreds.getPrivateKeyId());
TestServiceGrpc.TestServiceBlockingStub stub = blockingStub
- .withInterceptors(new ClientAuthInterceptor(credentials,
- Executors.newSingleThreadExecutor()));
+ .withCallCredentials(MoreCallCredentials.from(credentials));
SimpleResponse response = stub.unaryCall(request);
assertEquals(origCreds.getClientEmail(), response.getUsername());
assertEquals(314159, response.getPayload().getBody().size());
@@ -904,8 +901,7 @@ public abstract class AbstractInteropTest {
};
TestServiceGrpc.TestServiceBlockingStub stub = blockingStub
- .withInterceptors(new ClientAuthInterceptor(credentials,
- Executors.newSingleThreadExecutor()));
+ .withCallCredentials(MoreCallCredentials.from(credentials));
final SimpleRequest request = SimpleRequest.newBuilder()
.setFillUsername(true)
.setFillOauthScope(true)