1 /* 2 * Copyright 2019, Google LLC 3 * 4 * Redistribution and use in source and binary forms, with or without 5 * modification, are permitted provided that the following conditions are 6 * met: 7 * 8 * * Redistributions of source code must retain the above copyright 9 * notice, this list of conditions and the following disclaimer. 10 * * Redistributions in binary form must reproduce the above 11 * copyright notice, this list of conditions and the following disclaimer 12 * in the documentation and/or other materials provided with the 13 * distribution. 14 * 15 * * Neither the name of Google LLC nor the names of its 16 * contributors may be used to endorse or promote products derived from 17 * this software without specific prior written permission. 18 * 19 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 20 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 21 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 22 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 23 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 24 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 25 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 26 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 27 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 28 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 */ 31 32 package com.google.auth.oauth2; 33 34 import static org.junit.Assert.assertEquals; 35 import static org.junit.Assert.assertNotNull; 36 import static org.junit.Assert.assertNull; 37 import static org.junit.Assert.assertSame; 38 import static org.junit.Assert.fail; 39 40 import com.google.api.client.json.JsonFactory; 41 import com.google.api.client.json.gson.GsonFactory; 42 import com.google.api.client.json.webtoken.JsonWebSignature; 43 import com.google.api.client.util.Clock; 44 import com.google.auth.http.AuthHttpConstants; 45 import java.io.IOException; 46 import java.security.PrivateKey; 47 import java.util.Collections; 48 import java.util.List; 49 import java.util.Map; 50 import org.junit.Test; 51 import org.junit.runner.RunWith; 52 import org.junit.runners.JUnit4; 53 54 @RunWith(JUnit4.class) 55 public class JwtCredentialsTest extends BaseSerializationTest { 56 private static final String PRIVATE_KEY_ID = "d84a4fefcf50791d4a90f2d7af17469d6282df9d"; 57 private static final String PRIVATE_KEY = 58 "-----BEGIN PRIVATE KEY-----\n" 59 + "MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBALX0PQoe1igW12i" 60 + "kv1bN/r9lN749y2ijmbc/mFHPyS3hNTyOCjDvBbXYbDhQJzWVUikh4mvGBA07qTj79Xc3yBDfKP2IeyYQIFe0t0" 61 + "zkd7R9Zdn98Y2rIQC47aAbDfubtkU1U72t4zL11kHvoa0/RuFZjncvlr42X7be7lYh4p3NAgMBAAECgYASk5wDw" 62 + "4Az2ZkmeuN6Fk/y9H+Lcb2pskJIXjrL533vrDWGOC48LrsThMQPv8cxBky8HFSEklPpkfTF95tpD43iVwJRB/Gr" 63 + "CtGTw65IfJ4/tI09h6zGc4yqvIo1cHX/LQ+SxKLGyir/dQM925rGt/VojxY5ryJR7GLbCzxPnJm/oQJBANwOCO6" 64 + "D2hy1LQYJhXh7O+RLtA/tSnT1xyMQsGT+uUCMiKS2bSKx2wxo9k7h3OegNJIu1q6nZ6AbxDK8H3+d0dUCQQDTrP" 65 + "SXagBxzp8PecbaCHjzNRSQE2in81qYnrAFNB4o3DpHyMMY6s5ALLeHKscEWnqP8Ur6X4PvzZecCWU9BKAZAkAut" 66 + "LPknAuxSCsUOvUfS1i87ex77Ot+w6POp34pEX+UWb+u5iFn2cQacDTHLV1LtE80L8jVLSbrbrlH43H0DjU5AkEA" 67 + "gidhycxS86dxpEljnOMCw8CKoUBd5I880IUahEiUltk7OLJYS/Ts1wbn3kPOVX3wyJs8WBDtBkFrDHW2ezth2QJ" 68 + "ADj3e1YhMVdjJW5jqwlD/VNddGjgzyunmiZg0uOXsHXbytYmsA545S8KRQFaJKFXYYFo2kOjqOiC1T2cAzMDjCQ" 69 + "==\n-----END PRIVATE KEY-----\n"; 70 private static final String JWT_ACCESS_PREFIX = 71 ServiceAccountJwtAccessCredentials.JWT_ACCESS_PREFIX; 72 private static final JsonFactory JSON_FACTORY = GsonFactory.getDefaultInstance(); 73 getPrivateKey()74 static PrivateKey getPrivateKey() { 75 try { 76 return OAuth2Utils.privateKeyFromPkcs8(PRIVATE_KEY); 77 } catch (IOException ex) { 78 return null; 79 } 80 } 81 82 @Test serialize()83 public void serialize() throws IOException, ClassNotFoundException { 84 JwtClaims claims = 85 JwtClaims.newBuilder() 86 .setAudience("some-audience") 87 .setIssuer("some-issuer") 88 .setSubject("some-subject") 89 .build(); 90 JwtCredentials credentials = 91 JwtCredentials.newBuilder() 92 .setJwtClaims(claims) 93 .setPrivateKey(getPrivateKey()) 94 .setPrivateKeyId(PRIVATE_KEY_ID) 95 .build(); 96 97 JwtCredentials deserializedCredentials = serializeAndDeserialize(credentials); 98 assertEquals(credentials, deserializedCredentials); 99 assertEquals(credentials.hashCode(), deserializedCredentials.hashCode()); 100 assertEquals(credentials.toString(), deserializedCredentials.toString()); 101 assertSame(deserializedCredentials.getClock(), Clock.SYSTEM); 102 } 103 104 @Test builder_requiresPrivateKey()105 public void builder_requiresPrivateKey() { 106 try { 107 JwtClaims claims = 108 JwtClaims.newBuilder() 109 .setAudience("some-audience") 110 .setIssuer("some-issuer") 111 .setSubject("some-subject") 112 .build(); 113 JwtCredentials.newBuilder().setJwtClaims(claims).setPrivateKeyId(PRIVATE_KEY_ID).build(); 114 fail("Should throw exception"); 115 } catch (NullPointerException ex) { 116 // expected 117 } 118 } 119 120 @Test builder_requiresClaims()121 public void builder_requiresClaims() { 122 try { 123 JwtCredentials.newBuilder() 124 .setPrivateKeyId(PRIVATE_KEY_ID) 125 .setPrivateKey(getPrivateKey()) 126 .build(); 127 fail("Should throw exception"); 128 } catch (NullPointerException ex) { 129 // expected 130 } 131 } 132 133 @Test builder_requiresCompleteClaims()134 public void builder_requiresCompleteClaims() { 135 try { 136 JwtClaims claims = JwtClaims.newBuilder().build(); 137 JwtCredentials.newBuilder() 138 .setJwtClaims(claims) 139 .setPrivateKeyId(PRIVATE_KEY_ID) 140 .setPrivateKey(getPrivateKey()) 141 .build(); 142 fail("Should throw exception"); 143 } catch (IllegalStateException ex) { 144 // expected 145 } 146 } 147 148 @Test jwtWithClaims_overwritesClaims()149 public void jwtWithClaims_overwritesClaims() throws IOException { 150 JwtClaims claims = 151 JwtClaims.newBuilder() 152 .setAudience("some-audience") 153 .setIssuer("some-issuer") 154 .setSubject("some-subject") 155 .build(); 156 JwtCredentials credentials = 157 JwtCredentials.newBuilder() 158 .setJwtClaims(claims) 159 .setPrivateKey(getPrivateKey()) 160 .setPrivateKeyId(PRIVATE_KEY_ID) 161 .build(); 162 JwtClaims claims2 = 163 JwtClaims.newBuilder() 164 .setAudience("some-audience2") 165 .setIssuer("some-issuer2") 166 .setSubject("some-subject2") 167 .build(); 168 JwtCredentials credentials2 = credentials.jwtWithClaims(claims2); 169 Map<String, List<String>> metadata = credentials2.getRequestMetadata(); 170 verifyJwtAccess(metadata, "some-audience2", "some-issuer2", "some-subject2", PRIVATE_KEY_ID); 171 } 172 173 @Test jwtWithClaims_defaultsClaims()174 public void jwtWithClaims_defaultsClaims() throws IOException { 175 JwtClaims claims = 176 JwtClaims.newBuilder() 177 .setAudience("some-audience") 178 .setIssuer("some-issuer") 179 .setSubject("some-subject") 180 .build(); 181 JwtCredentials credentials = 182 JwtCredentials.newBuilder() 183 .setJwtClaims(claims) 184 .setPrivateKey(getPrivateKey()) 185 .setPrivateKeyId(PRIVATE_KEY_ID) 186 .build(); 187 JwtClaims claims2 = JwtClaims.newBuilder().build(); 188 JwtCredentials credentials2 = credentials.jwtWithClaims(claims2); 189 Map<String, List<String>> metadata = credentials2.getRequestMetadata(); 190 verifyJwtAccess(metadata, "some-audience", "some-issuer", "some-subject", PRIVATE_KEY_ID); 191 } 192 193 @Test getRequestMetadata_hasJwtAccess()194 public void getRequestMetadata_hasJwtAccess() throws IOException { 195 JwtClaims claims = 196 JwtClaims.newBuilder() 197 .setAudience("some-audience") 198 .setIssuer("some-issuer") 199 .setSubject("some-subject") 200 .build(); 201 JwtCredentials credentials = 202 JwtCredentials.newBuilder() 203 .setJwtClaims(claims) 204 .setPrivateKey(getPrivateKey()) 205 .setPrivateKeyId(PRIVATE_KEY_ID) 206 .build(); 207 208 Map<String, List<String>> metadata = credentials.getRequestMetadata(); 209 verifyJwtAccess(metadata, "some-audience", "some-issuer", "some-subject", PRIVATE_KEY_ID); 210 } 211 212 @Test getRequestMetadata_withAdditionalClaims_hasJwtAccess()213 public void getRequestMetadata_withAdditionalClaims_hasJwtAccess() throws IOException { 214 JwtClaims claims = 215 JwtClaims.newBuilder() 216 .setAudience("some-audience") 217 .setIssuer("some-issuer") 218 .setSubject("some-subject") 219 .setAdditionalClaims(Collections.singletonMap("foo", "bar")) 220 .build(); 221 JwtCredentials credentials = 222 JwtCredentials.newBuilder() 223 .setJwtClaims(claims) 224 .setPrivateKey(getPrivateKey()) 225 .setPrivateKeyId(PRIVATE_KEY_ID) 226 .build(); 227 228 Map<String, List<String>> metadata = credentials.getRequestMetadata(); 229 verifyJwtAccess( 230 metadata, 231 "some-audience", 232 "some-issuer", 233 "some-subject", 234 PRIVATE_KEY_ID, 235 Collections.singletonMap("foo", "bar")); 236 } 237 238 @Test privateKeyIdNull()239 public void privateKeyIdNull() throws IOException { 240 JwtClaims claims = 241 JwtClaims.newBuilder() 242 .setAudience("some-audience") 243 .setIssuer("some-issuer") 244 .setSubject("some-subject") 245 .build(); 246 JwtCredentials credentials = 247 JwtCredentials.newBuilder() 248 .setJwtClaims(claims) 249 .setPrivateKey(getPrivateKey()) 250 .setPrivateKeyId(null) 251 .build(); 252 253 Map<String, List<String>> metadata = credentials.getRequestMetadata(); 254 verifyJwtAccess(metadata, "some-audience", "some-issuer", "some-subject", null); 255 } 256 257 @Test privateKeyIdNotSpecified()258 public void privateKeyIdNotSpecified() throws IOException { 259 JwtClaims claims = 260 JwtClaims.newBuilder() 261 .setAudience("some-audience") 262 .setIssuer("some-issuer") 263 .setSubject("some-subject") 264 .build(); 265 JwtCredentials credentials = 266 JwtCredentials.newBuilder().setJwtClaims(claims).setPrivateKey(getPrivateKey()).build(); 267 268 Map<String, List<String>> metadata = credentials.getRequestMetadata(); 269 verifyJwtAccess(metadata, "some-audience", "some-issuer", "some-subject", null); 270 } 271 verifyJwtAccess( Map<String, List<String>> metadata, String expectedAudience, String expectedIssuer, String expectedSubject, String expectedKeyId)272 private void verifyJwtAccess( 273 Map<String, List<String>> metadata, 274 String expectedAudience, 275 String expectedIssuer, 276 String expectedSubject, 277 String expectedKeyId) 278 throws IOException { 279 verifyJwtAccess( 280 metadata, 281 expectedAudience, 282 expectedIssuer, 283 expectedSubject, 284 expectedKeyId, 285 Collections.<String, String>emptyMap()); 286 } 287 verifyJwtAccess( Map<String, List<String>> metadata, String expectedAudience, String expectedIssuer, String expectedSubject, String expectedKeyId, Map<String, String> expectedAdditionalClaims)288 private void verifyJwtAccess( 289 Map<String, List<String>> metadata, 290 String expectedAudience, 291 String expectedIssuer, 292 String expectedSubject, 293 String expectedKeyId, 294 Map<String, String> expectedAdditionalClaims) 295 throws IOException { 296 assertNotNull(metadata); 297 List<String> authorizations = metadata.get(AuthHttpConstants.AUTHORIZATION); 298 assertNotNull("Authorization headers not found", authorizations); 299 String assertion = null; 300 for (String authorization : authorizations) { 301 if (authorization.startsWith(JWT_ACCESS_PREFIX)) { 302 assertNull("Multiple bearer assertions found", assertion); 303 assertion = authorization.substring(JWT_ACCESS_PREFIX.length()); 304 } 305 } 306 assertNotNull("Bearer assertion not found", assertion); 307 JsonWebSignature signature = JsonWebSignature.parse(JSON_FACTORY, assertion); 308 assertEquals(expectedIssuer, signature.getPayload().getIssuer()); 309 assertEquals(expectedSubject, signature.getPayload().getSubject()); 310 assertEquals(expectedAudience, signature.getPayload().getAudience()); 311 assertEquals(expectedKeyId, signature.getHeader().getKeyId()); 312 313 for (Map.Entry<String, String> entry : expectedAdditionalClaims.entrySet()) { 314 assertEquals(entry.getValue(), signature.getPayload().get(entry.getKey())); 315 } 316 } 317 } 318