• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright 2016 The gRPC Authors
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *     http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package io.grpc.auth;
18 
19 import static com.google.common.base.Charsets.US_ASCII;
20 import static org.junit.Assert.assertArrayEquals;
21 import static org.junit.Assert.assertEquals;
22 import static org.junit.Assert.assertNull;
23 import static org.junit.Assert.assertTrue;
24 import static org.mockito.Matchers.any;
25 import static org.mockito.Matchers.eq;
26 import static org.mockito.Mockito.doAnswer;
27 import static org.mockito.Mockito.times;
28 import static org.mockito.Mockito.verify;
29 import static org.mockito.Mockito.when;
30 
31 import com.google.auth.Credentials;
32 import com.google.auth.RequestMetadataCallback;
33 import com.google.auth.oauth2.AccessToken;
34 import com.google.auth.oauth2.GoogleCredentials;
35 import com.google.auth.oauth2.OAuth2Credentials;
36 import com.google.auth.oauth2.ServiceAccountCredentials;
37 import com.google.common.collect.Iterables;
38 import com.google.common.collect.LinkedListMultimap;
39 import com.google.common.collect.ListMultimap;
40 import com.google.common.collect.Multimaps;
41 import io.grpc.Attributes;
42 import io.grpc.CallCredentials2;
43 import io.grpc.CallCredentials2.MetadataApplier;
44 import io.grpc.Metadata;
45 import io.grpc.MethodDescriptor;
46 import io.grpc.SecurityLevel;
47 import io.grpc.Status;
48 import io.grpc.testing.TestMethodDescriptors;
49 import java.io.IOException;
50 import java.net.URI;
51 import java.security.KeyPair;
52 import java.security.KeyPairGenerator;
53 import java.util.ArrayList;
54 import java.util.Arrays;
55 import java.util.Date;
56 import java.util.List;
57 import java.util.Map;
58 import java.util.concurrent.Executor;
59 import org.junit.After;
60 import org.junit.Before;
61 import org.junit.Test;
62 import org.junit.runner.RunWith;
63 import org.junit.runners.JUnit4;
64 import org.mockito.ArgumentCaptor;
65 import org.mockito.Captor;
66 import org.mockito.Mock;
67 import org.mockito.MockitoAnnotations;
68 import org.mockito.invocation.InvocationOnMock;
69 import org.mockito.stubbing.Answer;
70 
71 /**
72  * Tests for {@link GoogleAuthLibraryCallCredentials}.
73  */
74 @RunWith(JUnit4.class)
75 public class GoogleAuthLibraryCallCredentialsTest {
76 
77   private static final Metadata.Key<String> AUTHORIZATION = Metadata.Key.of("Authorization",
78       Metadata.ASCII_STRING_MARSHALLER);
79   private static final Metadata.Key<byte[]> EXTRA_AUTHORIZATION = Metadata.Key.of(
80       "Extra-Authorization-bin", Metadata.BINARY_BYTE_MARSHALLER);
81 
82   @Mock
83   private Credentials credentials;
84 
85   @Mock
86   private MetadataApplier applier;
87 
88   private Executor executor = new Executor() {
89     @Override public void execute(Runnable r) {
90       pendingRunnables.add(r);
91     }
92   };
93 
94   @Captor
95   private ArgumentCaptor<Metadata> headersCaptor;
96 
97   @Captor
98   private ArgumentCaptor<Status> statusCaptor;
99 
100   private MethodDescriptor<Void, Void> method = MethodDescriptor.<Void, Void>newBuilder()
101       .setType(MethodDescriptor.MethodType.UNKNOWN)
102       .setFullMethodName("a.service/method")
103       .setRequestMarshaller(TestMethodDescriptors.voidMarshaller())
104       .setResponseMarshaller(TestMethodDescriptors.voidMarshaller())
105       .build();
106   private URI expectedUri = URI.create("https://testauthority/a.service");
107 
108   private static final String AUTHORITY = "testauthority";
109   private static final SecurityLevel SECURITY_LEVEL = SecurityLevel.PRIVACY_AND_INTEGRITY;
110 
111   private ArrayList<Runnable> pendingRunnables = new ArrayList<>();
112 
113   @Before
setUp()114   public void setUp() throws Exception {
115     MockitoAnnotations.initMocks(this);
116     doAnswer(new Answer<Void>() {
117       @Override
118       public Void answer(InvocationOnMock invocation) {
119         Credentials mock = (Credentials) invocation.getMock();
120         URI uri = (URI) invocation.getArguments()[0];
121         RequestMetadataCallback callback = (RequestMetadataCallback) invocation.getArguments()[2];
122         Map<String, List<String>> metadata;
123         try {
124           // Default to calling the blocking method, since it is easier to mock
125           metadata = mock.getRequestMetadata(uri);
126         } catch (Exception ex) {
127           callback.onFailure(ex);
128           return null;
129         }
130         callback.onSuccess(metadata);
131         return null;
132       }
133     }).when(credentials).getRequestMetadata(
134         any(URI.class),
135         any(Executor.class),
136         any(RequestMetadataCallback.class));
137   }
138 
139   @After
tearDown()140   public void tearDown() {
141     assertEquals(0, pendingRunnables.size());
142   }
143 
144   @Test
copyCredentialsToHeaders()145   public void copyCredentialsToHeaders() throws Exception {
146     ListMultimap<String, String> values = LinkedListMultimap.create();
147     values.put("Authorization", "token1");
148     values.put("Authorization", "token2");
149     values.put("Extra-Authorization-bin", "dG9rZW4z");  // bytes "token3" in base64
150     values.put("Extra-Authorization-bin", "dG9rZW40");  // bytes "token4" in base64
151     when(credentials.getRequestMetadata(eq(expectedUri))).thenReturn(Multimaps.asMap(values));
152 
153     GoogleAuthLibraryCallCredentials callCredentials =
154         new GoogleAuthLibraryCallCredentials(credentials);
155     callCredentials.applyRequestMetadata(new RequestInfoImpl(), executor, applier);
156 
157     verify(credentials).getRequestMetadata(eq(expectedUri));
158     verify(applier).apply(headersCaptor.capture());
159     Metadata headers = headersCaptor.getValue();
160     Iterable<String> authorization = headers.getAll(AUTHORIZATION);
161     assertArrayEquals(new String[]{"token1", "token2"},
162         Iterables.toArray(authorization, String.class));
163     Iterable<byte[]> extraAuthorization = headers.getAll(EXTRA_AUTHORIZATION);
164     assertEquals(2, Iterables.size(extraAuthorization));
165     assertArrayEquals("token3".getBytes(US_ASCII), Iterables.get(extraAuthorization, 0));
166     assertArrayEquals("token4".getBytes(US_ASCII), Iterables.get(extraAuthorization, 1));
167   }
168 
169   @Test
invalidBase64()170   public void invalidBase64() throws Exception {
171     ListMultimap<String, String> values = LinkedListMultimap.create();
172     values.put("Extra-Authorization-bin", "dG9rZW4z1");  // invalid base64
173     when(credentials.getRequestMetadata(eq(expectedUri))).thenReturn(Multimaps.asMap(values));
174 
175     GoogleAuthLibraryCallCredentials callCredentials =
176         new GoogleAuthLibraryCallCredentials(credentials);
177     callCredentials.applyRequestMetadata(new RequestInfoImpl(), executor, applier);
178 
179     verify(credentials).getRequestMetadata(eq(expectedUri));
180     verify(applier).fail(statusCaptor.capture());
181     Status status = statusCaptor.getValue();
182     assertEquals(Status.Code.UNAUTHENTICATED, status.getCode());
183     assertEquals(IllegalArgumentException.class, status.getCause().getClass());
184   }
185 
186   @Test
credentialsFailsWithIoException()187   public void credentialsFailsWithIoException() throws Exception {
188     Exception exception = new IOException("Broken");
189     when(credentials.getRequestMetadata(eq(expectedUri))).thenThrow(exception);
190 
191     GoogleAuthLibraryCallCredentials callCredentials =
192         new GoogleAuthLibraryCallCredentials(credentials);
193     callCredentials.applyRequestMetadata(new RequestInfoImpl(), executor, applier);
194 
195     verify(credentials).getRequestMetadata(eq(expectedUri));
196     verify(applier).fail(statusCaptor.capture());
197     Status status = statusCaptor.getValue();
198     assertEquals(Status.Code.UNAVAILABLE, status.getCode());
199     assertEquals(exception, status.getCause());
200   }
201 
202   @Test
credentialsFailsWithRuntimeException()203   public void credentialsFailsWithRuntimeException() throws Exception {
204     Exception exception = new RuntimeException("Broken");
205     when(credentials.getRequestMetadata(eq(expectedUri))).thenThrow(exception);
206 
207     GoogleAuthLibraryCallCredentials callCredentials =
208         new GoogleAuthLibraryCallCredentials(credentials);
209     callCredentials.applyRequestMetadata(new RequestInfoImpl(), executor, applier);
210 
211     verify(credentials).getRequestMetadata(eq(expectedUri));
212     verify(applier).fail(statusCaptor.capture());
213     Status status = statusCaptor.getValue();
214     assertEquals(Status.Code.UNAUTHENTICATED, status.getCode());
215     assertEquals(exception, status.getCause());
216   }
217 
218   @Test
219   @SuppressWarnings("unchecked")
credentialsReturnNullMetadata()220   public void credentialsReturnNullMetadata() throws Exception {
221     ListMultimap<String, String> values = LinkedListMultimap.create();
222     values.put("Authorization", "token1");
223     when(credentials.getRequestMetadata(eq(expectedUri)))
224         .thenReturn(null, Multimaps.<String, String>asMap(values), null);
225 
226     GoogleAuthLibraryCallCredentials callCredentials =
227         new GoogleAuthLibraryCallCredentials(credentials);
228     for (int i = 0; i < 3; i++) {
229       callCredentials.applyRequestMetadata(new RequestInfoImpl(), executor, applier);
230     }
231 
232     verify(credentials, times(3)).getRequestMetadata(eq(expectedUri));
233 
234     verify(applier, times(3)).apply(headersCaptor.capture());
235     List<Metadata> headerList = headersCaptor.getAllValues();
236     assertEquals(3, headerList.size());
237 
238     assertEquals(0, headerList.get(0).keys().size());
239 
240     Iterable<String> authorization = headerList.get(1).getAll(AUTHORIZATION);
241     assertArrayEquals(new String[]{"token1"}, Iterables.toArray(authorization, String.class));
242 
243     assertEquals(0, headerList.get(2).keys().size());
244   }
245 
246   @Test
oauth2Credential()247   public void oauth2Credential() {
248     final AccessToken token = new AccessToken("allyourbase", new Date(Long.MAX_VALUE));
249     final OAuth2Credentials credentials = new OAuth2Credentials() {
250       @Override
251       public AccessToken refreshAccessToken() throws IOException {
252         return token;
253       }
254     };
255 
256     GoogleAuthLibraryCallCredentials callCredentials =
257         new GoogleAuthLibraryCallCredentials(credentials);
258     callCredentials.applyRequestMetadata(
259         new RequestInfoImpl(SecurityLevel.NONE), executor, applier);
260     assertEquals(1, runPendingRunnables());
261 
262     verify(applier).apply(headersCaptor.capture());
263     Metadata headers = headersCaptor.getValue();
264     Iterable<String> authorization = headers.getAll(AUTHORIZATION);
265     assertArrayEquals(new String[]{"Bearer allyourbase"},
266         Iterables.toArray(authorization, String.class));
267   }
268 
269   @Test
googleCredential_privacyAndIntegrityAllowed()270   public void googleCredential_privacyAndIntegrityAllowed() {
271     final AccessToken token = new AccessToken("allyourbase", new Date(Long.MAX_VALUE));
272     final Credentials credentials = GoogleCredentials.create(token);
273 
274     GoogleAuthLibraryCallCredentials callCredentials =
275         new GoogleAuthLibraryCallCredentials(credentials);
276     callCredentials.applyRequestMetadata(
277         new RequestInfoImpl(SecurityLevel.PRIVACY_AND_INTEGRITY), executor, applier);
278     runPendingRunnables();
279 
280     verify(applier).apply(headersCaptor.capture());
281     Metadata headers = headersCaptor.getValue();
282     Iterable<String> authorization = headers.getAll(AUTHORIZATION);
283     assertArrayEquals(new String[]{"Bearer allyourbase"},
284         Iterables.toArray(authorization, String.class));
285   }
286 
287   @Test
googleCredential_integrityDenied()288   public void googleCredential_integrityDenied() {
289     final AccessToken token = new AccessToken("allyourbase", new Date(Long.MAX_VALUE));
290     final Credentials credentials = GoogleCredentials.create(token);
291     // Anything less than PRIVACY_AND_INTEGRITY should fail
292 
293     GoogleAuthLibraryCallCredentials callCredentials =
294         new GoogleAuthLibraryCallCredentials(credentials);
295     callCredentials.applyRequestMetadata(
296         new RequestInfoImpl(SecurityLevel.INTEGRITY), executor, applier);
297     runPendingRunnables();
298 
299     verify(applier).fail(statusCaptor.capture());
300     Status status = statusCaptor.getValue();
301     assertEquals(Status.Code.UNAUTHENTICATED, status.getCode());
302   }
303 
304   @Test
serviceUri()305   public void serviceUri() throws Exception {
306     GoogleAuthLibraryCallCredentials callCredentials =
307         new GoogleAuthLibraryCallCredentials(credentials);
308     callCredentials.applyRequestMetadata(
309         new RequestInfoImpl("example.com:443"), executor, applier);
310     verify(credentials).getRequestMetadata(eq(new URI("https://example.com/a.service")));
311 
312     callCredentials.applyRequestMetadata(
313         new RequestInfoImpl("example.com:123"), executor, applier);
314     verify(credentials).getRequestMetadata(eq(new URI("https://example.com:123/a.service")));
315   }
316 
317   @Test
serviceAccountToJwt()318   public void serviceAccountToJwt() throws Exception {
319     KeyPair pair = KeyPairGenerator.getInstance("RSA").generateKeyPair();
320     @SuppressWarnings("deprecation")
321     ServiceAccountCredentials credentials = new ServiceAccountCredentials(
322         null, "email@example.com", pair.getPrivate(), null, null) {
323       @Override
324       public AccessToken refreshAccessToken() {
325         throw new AssertionError();
326       }
327     };
328 
329     GoogleAuthLibraryCallCredentials callCredentials =
330         new GoogleAuthLibraryCallCredentials(credentials);
331     callCredentials.applyRequestMetadata(new RequestInfoImpl(), executor, applier);
332     assertEquals(0, runPendingRunnables());
333 
334     verify(applier).apply(headersCaptor.capture());
335     Metadata headers = headersCaptor.getValue();
336     String[] authorization = Iterables.toArray(headers.getAll(AUTHORIZATION), String.class);
337     assertEquals(1, authorization.length);
338     assertTrue(authorization[0], authorization[0].startsWith("Bearer "));
339     // JWT is reasonably long. Normal tokens aren't.
340     assertTrue(authorization[0], authorization[0].length() > 300);
341   }
342 
343   @Test
serviceAccountWithScopeNotToJwt()344   public void serviceAccountWithScopeNotToJwt() throws Exception {
345     final AccessToken token = new AccessToken("allyourbase", new Date(Long.MAX_VALUE));
346     KeyPair pair = KeyPairGenerator.getInstance("RSA").generateKeyPair();
347     @SuppressWarnings("deprecation")
348     ServiceAccountCredentials credentials = new ServiceAccountCredentials(
349         null, "email@example.com", pair.getPrivate(), null, Arrays.asList("somescope")) {
350       @Override
351       public AccessToken refreshAccessToken() {
352         return token;
353       }
354     };
355 
356     GoogleAuthLibraryCallCredentials callCredentials =
357         new GoogleAuthLibraryCallCredentials(credentials);
358     callCredentials.applyRequestMetadata(new RequestInfoImpl(), executor, applier);
359     assertEquals(1, runPendingRunnables());
360 
361     verify(applier).apply(headersCaptor.capture());
362     Metadata headers = headersCaptor.getValue();
363     Iterable<String> authorization = headers.getAll(AUTHORIZATION);
364     assertArrayEquals(new String[]{"Bearer allyourbase"},
365         Iterables.toArray(authorization, String.class));
366   }
367 
368   @Test
oauthClassesNotInClassPath()369   public void oauthClassesNotInClassPath() throws Exception {
370     ListMultimap<String, String> values = LinkedListMultimap.create();
371     values.put("Authorization", "token1");
372     when(credentials.getRequestMetadata(eq(expectedUri))).thenReturn(Multimaps.asMap(values));
373 
374     assertNull(GoogleAuthLibraryCallCredentials.createJwtHelperOrNull(null));
375     GoogleAuthLibraryCallCredentials callCredentials =
376         new GoogleAuthLibraryCallCredentials(credentials, null);
377     callCredentials.applyRequestMetadata(new RequestInfoImpl(), executor, applier);
378 
379     verify(credentials).getRequestMetadata(eq(expectedUri));
380     verify(applier).apply(headersCaptor.capture());
381     Metadata headers = headersCaptor.getValue();
382     Iterable<String> authorization = headers.getAll(AUTHORIZATION);
383     assertArrayEquals(new String[]{"token1"},
384         Iterables.toArray(authorization, String.class));
385   }
386 
runPendingRunnables()387   private int runPendingRunnables() {
388     ArrayList<Runnable> savedPendingRunnables = pendingRunnables;
389     pendingRunnables = new ArrayList<>();
390     for (Runnable r : savedPendingRunnables) {
391       r.run();
392     }
393     return savedPendingRunnables.size();
394   }
395 
396   private final class RequestInfoImpl extends CallCredentials2.RequestInfo {
397     final String authority;
398     final SecurityLevel securityLevel;
399 
RequestInfoImpl()400     RequestInfoImpl() {
401       this(AUTHORITY, SECURITY_LEVEL);
402     }
403 
RequestInfoImpl(SecurityLevel securityLevel)404     RequestInfoImpl(SecurityLevel securityLevel) {
405       this(AUTHORITY, securityLevel);
406     }
407 
RequestInfoImpl(String authority)408     RequestInfoImpl(String authority) {
409       this(authority, SECURITY_LEVEL);
410     }
411 
RequestInfoImpl(String authority, SecurityLevel securityLevel)412     RequestInfoImpl(String authority, SecurityLevel securityLevel) {
413       this.authority = authority;
414       this.securityLevel = securityLevel;
415     }
416 
417     @Override
getMethodDescriptor()418     public MethodDescriptor<?, ?> getMethodDescriptor() {
419       return method;
420     }
421 
422     @Override
getSecurityLevel()423     public SecurityLevel getSecurityLevel() {
424       return securityLevel;
425     }
426 
427     @Override
getAuthority()428     public String getAuthority() {
429       return authority;
430     }
431 
432     @Override
getTransportAttrs()433     public Attributes getTransportAttrs() {
434       return Attributes.EMPTY;
435     }
436   }
437 }
438