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