blob: 92ad1b72ac1899dede0f3452ee905f23aa568aa9 [file] [log] [blame]
// Copyright 2020 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
////////////////////////////////////////////////////////////////////////////////
package com.google.crypto.tink.jwt;
import static com.google.common.truth.Truth.assertThat;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.junit.Assert.assertThrows;
import com.google.crypto.tink.proto.OutputPrefixType;
import com.google.crypto.tink.subtle.Base64;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import java.security.InvalidAlgorithmParameterException;
import java.util.Optional;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
/** Unit tests for JwtFormat */
@RunWith(JUnit4.class)
public final class JwtFormatTest {
@Test
public void createDecodeHeader_success() throws Exception {
String header =
JwtFormat.decodeHeader(JwtFormat.createHeader("RS256", Optional.empty(), Optional.empty()));
assertThat(header).isEqualTo("{\"alg\":\"RS256\"}");
}
@Test
public void createDecodeHeaderWithTyp_success() throws Exception {
String header =
JwtFormat.decodeHeader(
JwtFormat.createHeader("RS256", Optional.of("JWT"), Optional.empty()));
assertThat(header).isEqualTo("{\"alg\":\"RS256\",\"typ\":\"JWT\"}");
}
@Test
public void createDecodeHeaderWithKidAndTyp_success() throws Exception {
String header =
JwtFormat.decodeHeader(
JwtFormat.createHeader("RS256", Optional.of("JWT"), Optional.of("GsapRA")));
assertThat(header).isEqualTo("{\"kid\":\"GsapRA\",\"alg\":\"RS256\",\"typ\":\"JWT\"}");
}
@Test
public void createDecodeHeaderWithInvalidUtf8_fails() throws Exception {
assertThrows(
JwtInvalidException.class,
() -> JwtFormat.decodeHeader("eyJhbGciOiJIUzI1NiIsICJhIjoiwiJ9"));
}
@Test
public void getKidFromTinkOutputPrefixType_success() throws Exception {
int keyId = 0x1ac6a944;
Optional<String> kid = JwtFormat.getKid(keyId, OutputPrefixType.TINK);
// ID Requirement(Hex): 1 a c 6 a 9 4 4
// ID Requirement(Binary): 0001 1010 1100 0110 1010 1001 0100 0100
// Regroup for base64: 000110 101100 011010 101001 010001 00 (+0000)
// Base64 encode: G s a p R A
assertThat(kid.get()).isEqualTo("GsapRA");
assertThat(JwtFormat.getKeyId(kid.get()).get()).isEqualTo(0x1ac6a944);
}
@Test
public void getKidFromRawOutputPrefixType_success() throws Exception {
int keyId = 0x1ac6a944;
Optional<String> kid = JwtFormat.getKid(keyId, OutputPrefixType.RAW);
assertThat(kid.isPresent()).isFalse();
}
@Test
public void keyIdKidConversion_outputIsEqual() throws Exception {
assertThat(JwtFormat.getKeyId(JwtFormat.getKid(0x12345678, OutputPrefixType.TINK).get()).get())
.isEqualTo(0x12345678);
assertThat(JwtFormat.getKeyId(JwtFormat.getKid(-2147483648, OutputPrefixType.TINK).get()).get())
.isEqualTo(-2147483648);
assertThat(JwtFormat.getKeyId(JwtFormat.getKid(-100, OutputPrefixType.TINK).get()).get())
.isEqualTo(-100);
assertThat(JwtFormat.getKeyId(JwtFormat.getKid(0, OutputPrefixType.TINK).get()).get())
.isEqualTo(0);
assertThat(JwtFormat.getKeyId(JwtFormat.getKid(100, OutputPrefixType.TINK).get()).get())
.isEqualTo(100);
assertThat(JwtFormat.getKeyId(JwtFormat.getKid(2147483647, OutputPrefixType.TINK).get()).get())
.isEqualTo(2147483647);
}
@Test
public void getKeyId_wrongFormat_isNotPresent() throws Exception {
assertThat(JwtFormat.getKeyId("GsapRAA").isPresent()).isFalse();
assertThat(JwtFormat.getKeyId("Gsap").isPresent()).isFalse();
assertThat(JwtFormat.getKeyId("").isPresent()).isFalse();
assertThat(JwtFormat.getKeyId("dBjftJeZ4CVP-mB92K27uhbUJU1p1r").isPresent()).isFalse();
}
@Test
public void getKidFromUnsupportedOutputPrefixType_fails() throws Exception {
int keyId = 0x1ac6a944;
assertThrows(JwtInvalidException.class, () -> JwtFormat.getKid(keyId, OutputPrefixType.LEGACY));
}
@Test
public void createHeaderWithUnknownAlgorithm_fails() throws Exception {
assertThrows(
InvalidAlgorithmParameterException.class,
() -> JwtFormat.createHeader("UnknownAlgorithm", Optional.empty(), Optional.empty()));
}
@Test
public void decodeHeaderA1_success() throws Exception {
// Example from https://tools.ietf.org/html/rfc7515#appendix-A.1
String header = JwtFormat.decodeHeader("eyJ0eXAiOiJKV1QiLA0KICJhbGciOiJIUzI1NiJ9");
assertThat(header).isEqualTo("{\"typ\":\"JWT\",\r\n \"alg\":\"HS256\"}");
}
@Test
public void decodeHeaderA2_success() throws Exception {
// Example from https://tools.ietf.org/html/rfc7515#appendix-A.2
String header = JwtFormat.decodeHeader("eyJhbGciOiJSUzI1NiJ9");
assertThat(header).isEqualTo("{\"alg\":\"RS256\"}");
}
@Test
public void decodeModifiedHeader_success() throws Exception {
assertThrows(JwtInvalidException.class, () -> JwtFormat.decodeHeader("eyJhbGciOiJSUzI1NiJ9?"));
assertThrows(JwtInvalidException.class, () -> JwtFormat.decodeHeader("eyJhbGciOiJ SUzI1NiJ9"));
assertThrows(
JwtInvalidException.class, () -> JwtFormat.decodeHeader("eyJhbGci\r\nOiJSUzI1NiJ9"));
}
@Test
public void decodeHeader_success() throws Exception {
String headerStr = Base64.urlSafeEncode("{\"alg\":\"RS256\"}".getBytes(UTF_8));
String header = JwtFormat.decodeHeader(headerStr);
assertThat(header).isEqualTo("{\"alg\":\"RS256\"}");
}
@Test
public void decodeInvalidHeader_fails() throws Exception {
assertThrows(JwtInvalidException.class, () -> JwtFormat.decodeHeader("?="));
}
@Test
public void createDecodeValidateHeader_success() throws Exception {
JwtFormat.validateHeader(
"HS256",
Optional.empty(), Optional.empty(),
JsonUtil.parseJson(
JwtFormat.decodeHeader(
JwtFormat.createHeader("HS256", Optional.empty(), Optional.empty()))));
JwtFormat.validateHeader(
"HS384",
Optional.empty(), Optional.empty(),
JsonUtil.parseJson(
JwtFormat.decodeHeader(
JwtFormat.createHeader("HS384", Optional.empty(), Optional.empty()))));
JwtFormat.validateHeader(
"HS512",
Optional.empty(), Optional.empty(),
JsonUtil.parseJson(
JwtFormat.decodeHeader(
JwtFormat.createHeader("HS512", Optional.empty(), Optional.empty()))));
JwtFormat.validateHeader(
"ES256",
Optional.empty(), Optional.empty(),
JsonUtil.parseJson(
JwtFormat.decodeHeader(
JwtFormat.createHeader("ES256", Optional.empty(), Optional.empty()))));
JwtFormat.validateHeader(
"RS256",
Optional.empty(), Optional.empty(),
JsonUtil.parseJson(
JwtFormat.decodeHeader(
JwtFormat.createHeader("RS256", Optional.empty(), Optional.empty()))));
}
@Test
public void validateHeaderWithWrongAlgorithm_fails() throws Exception {
String header =
JwtFormat.decodeHeader(JwtFormat.createHeader("HS256", Optional.empty(), Optional.empty()));
assertThrows(
InvalidAlgorithmParameterException.class,
() ->
JwtFormat.validateHeader(
"HS384", Optional.empty(), Optional.empty(), JsonUtil.parseJson(header)));
}
@Test
public void validateHeaderWithUnknownAlgorithm_fails() throws Exception {
assertThrows(
InvalidAlgorithmParameterException.class,
() ->
JwtFormat.validateHeader(
"UnknownAlgorithm",
Optional.empty(), Optional.empty(),
JsonUtil.parseJson("{\"alg\": \"UnknownAlgorithm\"}")));
}
@Test
public void validateHeaderIgnoresTyp() throws Exception {
JwtFormat.validateHeader(
"HS256",
Optional.empty(), Optional.empty(),
JsonUtil.parseJson("{\"alg\": \"HS256\", \"typ\": \"unknown\"}"));
}
@Test
public void validateHeaderRejectsCrit() throws Exception {
assertThrows(
JwtInvalidException.class,
() ->
JwtFormat.validateHeader(
"HS256",
Optional.empty(), Optional.empty(),
JsonUtil.parseJson(
"{\"alg\": \"HS256\", \"crit\":[\"http://example.invalid/UNDEFINED\"], "
+ "\"http://example.invalid/UNDEFINED\":true}")));
}
@Test
public void validateHeaderWithUnknownEntry_success() throws Exception {
JwtFormat.validateHeader(
"HS256",
Optional.empty(), Optional.empty(),
JsonUtil.parseJson("{\"alg\": \"HS256\", \"unknown\": \"header\"}"));
}
@Test
public void validateEmptyHeader_fails() throws Exception {
assertThrows(
JwtInvalidException.class,
() ->
JwtFormat.validateHeader(
"HS256", Optional.empty(), Optional.empty(), JsonUtil.parseJson("{}")));
}
@Test
public void validateHeaderWithTinkKid() throws Exception {
JwtFormat.validateHeader(
"HS256",
Optional.of("kid123"), Optional.empty(),
JsonUtil.parseJson("{\"alg\": \"HS256\", \"kid\": \"kid123\"}"));
assertThrows(
JwtInvalidException.class,
() ->
JwtFormat.validateHeader(
"HS256",
Optional.of("kid123"),
Optional.empty(),
JsonUtil.parseJson("{\"alg\": \"HS256\", \"kid\": \"wrongKid\"}")));
// If tinkKid is set, then the kid is required in the header.
assertThrows(
JwtInvalidException.class,
() ->
JwtFormat.validateHeader(
"HS256",
Optional.of("kid123"),
Optional.empty(),
JsonUtil.parseJson("{\"alg\": \"HS256\"}")));
}
@Test
public void validateHeaderWithCustomKid() throws Exception {
JwtFormat.validateHeader(
"HS256",
Optional.empty(), Optional.of("kid123"),
JsonUtil.parseJson("{\"alg\": \"HS256\", \"kid\": \"kid123\"}"));
assertThrows(
JwtInvalidException.class,
() ->
JwtFormat.validateHeader(
"HS256",
Optional.empty(),
Optional.of("kid123"),
JsonUtil.parseJson("{\"alg\": \"HS256\", \"kid\": \"wrongKid\"}")));
// If customKid is set, then the kid is not required in the header.
JwtFormat.validateHeader(
"HS256",
Optional.empty(), Optional.of("kid123"),
JsonUtil.parseJson("{\"alg\": \"HS256\"}"));
}
@Test
public void validateHeaderWithBothTinkAndCustomKid_fails() throws Exception {
assertThrows(
JwtInvalidException.class,
() ->
JwtFormat.validateHeader(
"HS256",
Optional.of("kid123"),
Optional.of("kid123"),
JsonUtil.parseJson("{\"alg\": \"HS256\", \"kid\": \"kid123\"}")));
}
@Test
public void encodeDecodePayload_equal() throws Exception {
JsonObject payload = new JsonObject();
payload.addProperty("iss", "joe");
payload.addProperty("exp", 1300819380);
payload.addProperty("http://example.com/is_root", true);
String jsonPayload = payload.toString();
String encodedPayload = JwtFormat.encodePayload(jsonPayload);
String decodedPayload = JwtFormat.decodePayload(encodedPayload);
assertThat(decodedPayload).isEqualTo(jsonPayload);
}
@Test
public void decodePayload_success() throws Exception {
// Example from https://tools.ietf.org/html/rfc7515#appendix-A.1
JsonObject payload =
JsonParser.parseString(
JwtFormat.decodePayload(
"eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0dHA6Ly9leGFt"
+ "cGxlLmNvbS9pc19yb290Ijp0cnVlfQ"))
.getAsJsonObject();
assertThat(payload.get("iss").getAsString()).isEqualTo("joe");
assertThat(payload.get("exp").getAsInt()).isEqualTo(1300819380);
assertThat(payload.get("http://example.com/is_root").getAsBoolean()).isTrue();
}
@Test
public void decodeInvalidPayload_fails() throws Exception {
assertThrows(JwtInvalidException.class, () -> JwtFormat.decodePayload("?="));
}
@Test
public void createDecodePayloadWithInvalidUtf8_fails() throws Exception {
assertThrows(JwtInvalidException.class, () -> JwtFormat.decodePayload("eyJpc3MiOiJqb2XCIn0"));
}
@Test
public void signedCompactCreateSplit_success() throws Exception {
RawJwt rawJwt = RawJwt.newBuilder().setIssuer("joe").withoutExpiration().build();
String encodedSignature = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk";
byte[] signature = JwtFormat.decodeSignature(encodedSignature);
String unsignedCompact = JwtFormat.createUnsignedCompact("RS256", Optional.empty(), rawJwt);
String signedCompact = JwtFormat.createSignedCompact(unsignedCompact, signature);
JwtFormat.Parts parts = JwtFormat.splitSignedCompact(signedCompact);
JwtFormat.validateHeader(
"RS256", Optional.empty(), Optional.empty(), JsonUtil.parseJson(parts.header));
assertThat(unsignedCompact).isEqualTo("eyJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJqb2UifQ");
assertThat(signedCompact).isEqualTo(
"eyJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJqb2UifQ.dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk");
assertThat(parts.unsignedCompact).isEqualTo(unsignedCompact);
assertThat(parts.signatureOrMac).isEqualTo(signature);
assertThat(parts.header).isEqualTo("{\"alg\":\"RS256\"}");
assertThat(parts.payload).isEqualTo("{\"iss\":\"joe\"}");
}
@Test
public void signedCompactCreateSplitWithTypeHeader_success() throws Exception {
RawJwt rawJwt =
RawJwt.newBuilder().setTypeHeader("JWT").setIssuer("joe").withoutExpiration().build();
String encodedSignature = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk";
byte[] signature = JwtFormat.decodeSignature(encodedSignature);
String unsignedCompact = JwtFormat.createUnsignedCompact("RS256", Optional.empty(), rawJwt);
String signedCompact = JwtFormat.createSignedCompact(unsignedCompact, signature);
JwtFormat.Parts parts = JwtFormat.splitSignedCompact(signedCompact);
JsonObject parsedHeader = JsonUtil.parseJson(parts.header);
JwtFormat.validateHeader("RS256", Optional.empty(), Optional.empty(), parsedHeader);
assertThat(parsedHeader.getAsJsonPrimitive("typ").getAsString()).isEqualTo("JWT");
assertThat(unsignedCompact)
.isEqualTo("eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJqb2UifQ");
assertThat(parts.unsignedCompact).isEqualTo(unsignedCompact);
assertThat(parts.signatureOrMac).isEqualTo(signature);
assertThat(parts.header).isEqualTo("{\"alg\":\"RS256\",\"typ\":\"JWT\"}");
assertThat(parts.payload).isEqualTo("{\"iss\":\"joe\"}");
}
@Test
public void signedCompactCreateSplitWithKeyIdentifier_success() throws Exception {
String kid = "AZxkm2U";
RawJwt rawJwt = RawJwt.newBuilder().setIssuer("joe").withoutExpiration().build();
String encodedSignature = "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk";
byte[] signature = JwtFormat.decodeSignature(encodedSignature);
String unsignedCompact = JwtFormat.createUnsignedCompact("RS256", Optional.of(kid), rawJwt);
String signedCompact = JwtFormat.createSignedCompact(unsignedCompact, signature);
JwtFormat.Parts parts = JwtFormat.splitSignedCompact(signedCompact);
JsonObject parsedHeader = JsonUtil.parseJson(parts.header);
JwtFormat.validateHeader("RS256", Optional.empty(), Optional.empty(), parsedHeader);
assertThat(unsignedCompact)
.isEqualTo("eyJraWQiOiJBWnhrbTJVIiwiYWxnIjoiUlMyNTYifQ.eyJpc3MiOiJqb2UifQ");
assertThat(signedCompact)
.isEqualTo(
"eyJraWQiOiJBWnhrbTJVIiwiYWxnIjoiUlMyNTYifQ.eyJpc3MiOiJqb2UifQ"
+ ".dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk");
assertThat(parts.unsignedCompact).isEqualTo(unsignedCompact);
assertThat(parts.signatureOrMac).isEqualTo(signature);
assertThat(parts.header).isEqualTo("{\"kid\":\"AZxkm2U\",\"alg\":\"RS256\"}");
assertThat(parts.payload).isEqualTo("{\"iss\":\"joe\"}");
}
@Test
public void splitEmptySignedCompact_success() throws Exception {
JwtFormat.Parts parts = JwtFormat.splitSignedCompact("..");
assertThat(parts.unsignedCompact).isEqualTo(".");
assertThat(parts.signatureOrMac).isEmpty();
assertThat(parts.header).isEmpty();
assertThat(parts.payload).isEmpty();
}
@Test
public void splitSignedCompactWithBadFormat_fails() throws Exception {
assertThrows(
JwtInvalidException.class,
() -> JwtFormat.splitSignedCompact("e30.e30.YWJj.abc"));
assertThrows(
JwtInvalidException.class,
() -> JwtFormat.splitSignedCompact("e30.e30.YWJj."));
assertThrows(
JwtInvalidException.class,
() -> JwtFormat.splitSignedCompact(".e30.e30.YWJj"));
assertThrows(
JwtInvalidException.class,
() -> JwtFormat.splitSignedCompact(".e30.e30."));
assertThrows(
JwtInvalidException.class,
() -> JwtFormat.splitSignedCompact("e30.e30"));
assertThrows(
JwtInvalidException.class,
() -> JwtFormat.splitSignedCompact("e30"));
assertThrows(
JwtInvalidException.class,
() -> JwtFormat.splitSignedCompact(""));
}
@Test
public void splitSignedCompactWithBadCharacters_fails() throws Exception {
// check that unmodified token works
JwtFormat.Parts parts = JwtFormat.splitSignedCompact("e30.e30.YWJj");
assertThat(parts.unsignedCompact).isEqualTo("e30.e30");
// add bad characters
assertThrows(JwtInvalidException.class, () -> JwtFormat.splitSignedCompact("{e30.e30.YWJj"));
assertThrows(JwtInvalidException.class, () -> JwtFormat.splitSignedCompact(" e30.e30.YWJj"));
assertThrows(JwtInvalidException.class, () -> JwtFormat.splitSignedCompact("e30. e30.YWJj"));
assertThrows(JwtInvalidException.class, () -> JwtFormat.splitSignedCompact("e30.e30.YWJj "));
assertThrows(JwtInvalidException.class, () -> JwtFormat.splitSignedCompact("e30.e30.\nYWJj"));
assertThrows(JwtInvalidException.class, () -> JwtFormat.splitSignedCompact("e30.\re30.YWJj"));
assertThrows(JwtInvalidException.class, () -> JwtFormat.splitSignedCompact("e30$.e30.YWJj"));
assertThrows(JwtInvalidException.class, () -> JwtFormat.splitSignedCompact("e30.$e30.YWJj"));
assertThrows(JwtInvalidException.class, () -> JwtFormat.splitSignedCompact("e30.e30.YWJj$"));
assertThrows(JwtInvalidException.class, () -> JwtFormat.splitSignedCompact("e30.e30.YWJj$"));
assertThrows(
JwtInvalidException.class, () -> JwtFormat.splitSignedCompact("e30.e30.YWJj\ud83c"));
}
@Test
public void encodeDecodeSignature_success() throws Exception {
// Example from https://tools.ietf.org/html/rfc7515#appendix-A.1
byte[] signatureBytes =
new byte[] {
(byte) 116,
(byte) 24,
(byte) 223,
(byte) 180,
(byte) 151,
(byte) 153,
(byte) 224,
(byte) 37,
(byte) 79,
(byte) 250,
(byte) 96,
(byte) 125,
(byte) 216,
(byte) 173,
(byte) 187,
(byte) 186,
(byte) 22,
(byte) 212,
(byte) 37,
(byte) 77,
(byte) 105,
(byte) 214,
(byte) 191,
(byte) 240,
(byte) 91,
(byte) 88,
(byte) 5,
(byte) 88,
(byte) 83,
(byte) 132,
(byte) 141,
(byte) 121
};
String encodeSignature = JwtFormat.encodeSignature(signatureBytes);
assertThat(encodeSignature)
.isEqualTo("dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk");
assertThat(JwtFormat.decodeSignature(encodeSignature))
.isEqualTo(signatureBytes);
}
}