1 // Copyright 2022 Google LLC 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 // 15 //////////////////////////////////////////////////////////////////////////////// 16 17 package com.google.crypto.tink.jwt; 18 19 import static com.google.common.truth.Truth.assertThat; 20 import static org.junit.Assert.assertThrows; 21 22 import com.google.crypto.tink.DeterministicAead; 23 import com.google.crypto.tink.InsecureSecretKeyAccess; 24 import com.google.crypto.tink.KeyTemplates; 25 import com.google.crypto.tink.KeysetHandle; 26 import com.google.crypto.tink.RegistryConfiguration; 27 import com.google.crypto.tink.TinkJsonProtoKeysetFormat; 28 import com.google.crypto.tink.daead.DeterministicAeadConfig; 29 import java.security.GeneralSecurityException; 30 import java.time.Clock; 31 import java.time.Instant; 32 import org.junit.BeforeClass; 33 import org.junit.experimental.theories.DataPoints; 34 import org.junit.experimental.theories.FromDataPoints; 35 import org.junit.experimental.theories.Theories; 36 import org.junit.experimental.theories.Theory; 37 import org.junit.runner.RunWith; 38 39 /** Unit tests for the jwt package. Uses only the public API. */ 40 @RunWith(Theories.class) 41 public final class JwtTest { 42 43 @BeforeClass setUp()44 public static void setUp() throws Exception { 45 JwtMacConfig.register(); 46 DeterministicAeadConfig.register(); // Needed for getPrimitiveFromNonMacKeyset_throws. 47 } 48 49 @DataPoints("jwt_mac_templates") 50 public static final String[] TEMPLATES = 51 new String[] { 52 "JWT_HS256", 53 "JWT_HS256_RAW", 54 "JWT_HS384", 55 "JWT_HS384_RAW", 56 "JWT_HS512", 57 "JWT_HS512_RAW", 58 }; 59 60 @Theory createComputeVerifyJwtMac(@romDataPoints"jwt_mac_templates") String templateName)61 public void createComputeVerifyJwtMac(@FromDataPoints("jwt_mac_templates") String templateName) 62 throws Exception { 63 KeysetHandle handle = KeysetHandle.generateNew(KeyTemplates.get(templateName)); 64 JwtMac jwtMac = handle.getPrimitive(RegistryConfiguration.get(), JwtMac.class); 65 Instant now = Clock.systemUTC().instant(); 66 RawJwt rawJwt = 67 RawJwt.newBuilder() 68 .setIssuer("issuer") 69 .addAudience("audience") 70 .setSubject("subject") 71 .addStringClaim("claimName", "claimValue") 72 .setExpiration(now.plusSeconds(100)) 73 .build(); 74 String token = jwtMac.computeMacAndEncode(rawJwt); 75 76 JwtValidator validator = 77 JwtValidator.newBuilder().expectIssuer("issuer").expectAudience("audience").build(); 78 VerifiedJwt verifiedJwt = jwtMac.verifyMacAndDecode(token, validator); 79 assertThat(verifiedJwt.getSubject()).isEqualTo("subject"); 80 assertThat(verifiedJwt.getStringClaim("claimName")).isEqualTo("claimValue"); 81 82 String expiredToken = 83 jwtMac.computeMacAndEncode( 84 RawJwt.newBuilder() 85 .setIssuer("issuer") 86 .addAudience("audience") 87 .setExpiration(now.minusSeconds(100)) 88 .build()); 89 assertThrows( 90 GeneralSecurityException.class, () -> jwtMac.verifyMacAndDecode(expiredToken, validator)); 91 92 String tokenWithInvalidIssuer = 93 jwtMac.computeMacAndEncode( 94 RawJwt.newBuilder() 95 .setIssuer("invalid") 96 .addAudience("audience") 97 .setSubject("subject") 98 .addStringClaim("claimName", "claimValue") 99 .setExpiration(now.minusSeconds(100)) 100 .build()); 101 assertThrows( 102 GeneralSecurityException.class, 103 () -> jwtMac.verifyMacAndDecode(tokenWithInvalidIssuer, validator)); 104 105 String tokenWithInvalidAudience = 106 jwtMac.computeMacAndEncode( 107 RawJwt.newBuilder() 108 .setIssuer("issuer") 109 .addAudience("invalid") 110 .setSubject("subject") 111 .addStringClaim("claimName", "claimValue") 112 .setExpiration(now.minusSeconds(100)) 113 .build()); 114 assertThrows( 115 GeneralSecurityException.class, 116 () -> jwtMac.verifyMacAndDecode(tokenWithInvalidAudience, validator)); 117 118 KeysetHandle otherHandle = KeysetHandle.generateNew(KeyTemplates.get(templateName)); 119 JwtMac otherJwtMac = otherHandle.getPrimitive(RegistryConfiguration.get(), JwtMac.class); 120 assertThrows( 121 GeneralSecurityException.class, () -> otherJwtMac.verifyMacAndDecode(token, validator)); 122 123 assertThrows( 124 GeneralSecurityException.class, () -> jwtMac.verifyMacAndDecode("invalid", validator)); 125 assertThrows( 126 GeneralSecurityException.class, () -> jwtMac.verifyMacAndDecode("", validator)); 127 } 128 129 // A keyset with one JWT MAC key, serialized in Tink's JSON format. 130 private static final String JSON_JWT_MAC_KEYSET = 131 "" 132 + "{" 133 + " \"primaryKeyId\": 1685620571," 134 + " \"key\": [" 135 + " {" 136 + " \"keyData\": {" 137 + " \"typeUrl\": \"type.googleapis.com/google.crypto.tink.JwtHmacKey\"," 138 + " \"value\": \"GiDmRwUiwKDsPHd+2mSHwlLfzvkgoV5meopVKp+GCbhHthAB\"," 139 + " \"keyMaterialType\": \"SYMMETRIC\"" 140 + " }," 141 + " \"status\": \"ENABLED\"," 142 + " \"keyId\": 1685620571," 143 + " \"outputPrefixType\": \"TINK\"" 144 + " }" 145 + " ]" 146 + "}"; 147 148 @Theory readKeysetComputeVerifyJwtMac_success()149 public void readKeysetComputeVerifyJwtMac_success() throws Exception { 150 KeysetHandle handle = 151 TinkJsonProtoKeysetFormat.parseKeyset(JSON_JWT_MAC_KEYSET, InsecureSecretKeyAccess.get()); 152 Instant now = Clock.systemUTC().instant(); 153 JwtMac jwtMac = handle.getPrimitive(RegistryConfiguration.get(), JwtMac.class); 154 RawJwt rawJwt = 155 RawJwt.newBuilder() 156 .setIssuer("issuer") 157 .addAudience("audience") 158 .setSubject("subject") 159 .setExpiration(now.plusSeconds(100)) 160 .build(); 161 String token = jwtMac.computeMacAndEncode(rawJwt); 162 163 JwtValidator validator = 164 JwtValidator.newBuilder().expectIssuer("issuer").expectAudience("audience").build(); 165 VerifiedJwt verifiedJwt = jwtMac.verifyMacAndDecode(token, validator); 166 assertThat(verifiedJwt.getSubject()).isEqualTo("subject"); 167 } 168 169 // A keyset with multiple keys. The first key is the same as in JSON_AEAD_KEYSET. 170 private static final String JSON_JWT_MAC_KEYSET_WITH_MULTIPLE_KEYS = 171 "" 172 + "{" 173 + " \"primaryKeyId\": 648866621," 174 + " \"key\": [" 175 + " {" 176 + " \"keyData\": {" 177 + " \"typeUrl\": \"type.googleapis.com/google.crypto.tink.JwtHmacKey\"," 178 + " \"value\": \"GiDmRwUiwKDsPHd+2mSHwlLfzvkgoV5meopVKp+GCbhHthAB\"," 179 + " \"keyMaterialType\": \"SYMMETRIC\"" 180 + " }," 181 + " \"status\": \"ENABLED\"," 182 + " \"keyId\": 1685620571," 183 + " \"outputPrefixType\": \"TINK\"" 184 + " }," 185 + " {" 186 + " \"keyData\": {" 187 + " \"typeUrl\": \"type.googleapis.com/google.crypto.tink.JwtHmacKey\"," 188 + " \"value\":" 189 + "\"GjBP5UIYeH40mAliduNPdvnkGqJci3mRpxjSHZ6jkBQ7ppuOGwpyBqsLobFspZOR+y0QAg==\"," 190 + " \"keyMaterialType\": \"SYMMETRIC\"" 191 + " }," 192 + " \"status\": \"ENABLED\"," 193 + " \"keyId\": 648866621," 194 + " \"outputPrefixType\": \"RAW\"" 195 + " }," 196 + " {" 197 + " \"keyData\": {" 198 + " \"typeUrl\": \"type.googleapis.com/google.crypto.tink.JwtHmacKey\"," 199 + " \"value\": \"GkAjSoAXaQXhp8oHfEBdPUxKLWIA1hYNc+905NFRt0tYbDcje8LlPdmfVi8" 200 + "Xno7+U1xc0EPPxGFGfKPcIetKccgoEAM=\"," 201 + " \"keyMaterialType\": \"SYMMETRIC\"" 202 + " }," 203 + " \"status\": \"ENABLED\"," 204 + " \"keyId\": 923678323," 205 + " \"outputPrefixType\": \"TINK\"" 206 + " }" 207 + " ]" 208 + "}"; 209 210 @Theory multipleKeysReadKeysetComputeVerifyJwtMac_success()211 public void multipleKeysReadKeysetComputeVerifyJwtMac_success() 212 throws Exception { 213 KeysetHandle handle = 214 TinkJsonProtoKeysetFormat.parseKeyset( 215 JSON_JWT_MAC_KEYSET_WITH_MULTIPLE_KEYS, InsecureSecretKeyAccess.get()); 216 Instant now = Clock.systemUTC().instant(); 217 JwtMac jwtMac = handle.getPrimitive(RegistryConfiguration.get(), JwtMac.class); 218 RawJwt rawJwt = 219 RawJwt.newBuilder() 220 .setIssuer("issuer") 221 .addAudience("audience") 222 .setSubject("subject") 223 .setExpiration(now.plusSeconds(100)) 224 .build(); 225 String token = jwtMac.computeMacAndEncode(rawJwt); 226 JwtValidator validator = 227 JwtValidator.newBuilder().expectIssuer("issuer").expectAudience("audience").build(); 228 VerifiedJwt verifiedJwt = jwtMac.verifyMacAndDecode(token, validator); 229 assertThat(verifiedJwt.getSubject()).isEqualTo("subject"); 230 231 // Also test that jwtMac can verify tokens computed with a non-primary key. We use 232 // JSON_JWT_MAC_KEYSET to compute a tag with the first key. 233 KeysetHandle handle1 = 234 TinkJsonProtoKeysetFormat.parseKeyset( 235 JSON_JWT_MAC_KEYSET, InsecureSecretKeyAccess.get()); 236 JwtMac jwtMac1 = handle1.getPrimitive(RegistryConfiguration.get(), JwtMac.class); 237 String token1 = jwtMac1.computeMacAndEncode(rawJwt); 238 assertThat(jwtMac.verifyMacAndDecode(token1, validator).getSubject()).isEqualTo("subject"); 239 } 240 241 // A keyset with a valid DeterministicAead key. This keyset can't be used with the Mac primitive. 242 private static final String JSON_DAEAD_KEYSET = 243 "" 244 + "{" 245 + " \"primaryKeyId\": 961932622," 246 + " \"key\": [" 247 + " {" 248 + " \"keyData\": {" 249 + " \"typeUrl\": \"type.googleapis.com/google.crypto.tink.AesSivKey\"," 250 + " \"keyMaterialType\": \"SYMMETRIC\"," 251 + " \"value\": \"EkCJ9r5iwc5uxq5ugFyrHXh5dijTa7qalWUgZ8Gf08RxNd545FjtLMYL7ObcaFtCS" 252 + "kvV2+7u6F2DN+kqUjAfkf2W\"" 253 + " }," 254 + " \"outputPrefixType\": \"TINK\"," 255 + " \"keyId\": 961932622," 256 + " \"status\": \"ENABLED\"" 257 + " }" 258 + " ]" 259 + "}"; 260 261 @Theory getPrimitiveFromNonMacKeyset_throws()262 public void getPrimitiveFromNonMacKeyset_throws() throws Exception { 263 KeysetHandle handle = 264 TinkJsonProtoKeysetFormat.parseKeyset( 265 JSON_DAEAD_KEYSET, InsecureSecretKeyAccess.get()); 266 // Test that the keyset can create a DeterministicAead primitive, but not a JwtMac. 267 Object unused = handle.getPrimitive(RegistryConfiguration.get(), DeterministicAead.class); 268 assertThrows( 269 GeneralSecurityException.class, 270 () -> handle.getPrimitive(RegistryConfiguration.get(), JwtMac.class)); 271 } 272 273 } 274