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