Skip to content
This repository has been archived by the owner on Apr 17, 2024. It is now read-only.

Commit

Permalink
Add type header to JWTs in C++.
Browse files Browse the repository at this point in the history
PiperOrigin-RevId: 374206249
  • Loading branch information
juergw authored and copybara-github committed May 17, 2021
1 parent ab8670a commit 8b1b361
Show file tree
Hide file tree
Showing 16 changed files with 260 additions and 99 deletions.
3 changes: 3 additions & 0 deletions cc/jwt/internal/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -87,13 +87,15 @@ cc_library(
"//util:status",
"//util:statusor",
"@com_google_absl//absl/strings",
"@com_google_protobuf//:protobuf",
],
)

cc_test(
name = "jwt_format_test",
srcs = ["jwt_format_test.cc"],
deps = [
":json_util",
":jwt_format",
"//util:test_matchers",
"//util:test_util",
Expand Down Expand Up @@ -528,6 +530,7 @@ cc_library(
hdrs = ["jwt_public_key_verify_impl.h"],
include_prefix = "tink/jwt/internal",
deps = [
":json_util",
":jwt_format",
"//:public_key_verify",
"//jwt:jwt_public_key_verify",
Expand Down
3 changes: 3 additions & 0 deletions cc/jwt/internal/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ tink_cc_library(
jwt_format.cc
jwt_format.h
DEPS
protobuf::libprotobuf
tink::jwt::internal::json_util
tink::util::status
tink::util::statusor
Expand All @@ -64,6 +65,7 @@ tink_cc_test(
NAME jwt_format_test
SRCS jwt_format_test.cc
DEPS
tink::jwt::internal::json_util
tink::jwt::internal::jwt_format
tink::util::test_matchers
tink::util::test_util
Expand Down Expand Up @@ -488,6 +490,7 @@ tink_cc_library(
jwt_public_key_verify_impl.cc
jwt_public_key_verify_impl.h
DEPS
tink::jwt::internal::json_util
tink::jwt::internal::jwt_format
tink::core::public_key_verify
tink::jwt::jwt_public_key_verify
Expand Down
46 changes: 33 additions & 13 deletions cc/jwt/internal/jwt_format.cc
Original file line number Diff line number Diff line change
Expand Up @@ -51,22 +51,29 @@ bool DecodeHeader(absl::string_view header, std::string* json_header) {
return StrictWebSafeBase64Unescape(header, json_header);
}

std::string CreateHeader(absl::string_view algorithm) {
std::string header = absl::StrCat(R"({"alg":")", algorithm, R"("})");
return EncodeHeader(header);
std::string CreateHeader(absl::string_view algorithm,
absl::optional<absl::string_view> type_header) {
google::protobuf::Struct header;
auto fields = header.mutable_fields();
if (type_header.has_value()) {
google::protobuf::Value type_value;
type_value.set_string_value(std::string(type_header.value()));
(*fields)["typ"] = type_value;
}
google::protobuf::Value alg_value;
alg_value.set_string_value(std::string(algorithm));
(*fields)["alg"] = alg_value;
util::StatusOr<std::string> json_or =
jwt_internal::ProtoStructToJsonString(header);
if (!json_or.ok()) {
// do something
}
return EncodeHeader(json_or.ValueOrDie());
}

util::Status ValidateHeader(absl::string_view encoded_header,
util::Status ValidateHeader(const google::protobuf::Struct& header,
absl::string_view algorithm) {
std::string json_header;
if (!DecodeHeader(encoded_header, &json_header)) {
return util::Status(util::error::INVALID_ARGUMENT, "invalid header");
}
auto proto_or = JsonStringToProtoStruct(json_header);
if (!proto_or.ok()) {
return proto_or.status();
}
auto fields = proto_or.ValueOrDie().fields();
auto fields = header.fields();
auto it = fields.find("alg");
if (it == fields.end()) {
return util::Status(util::error::INVALID_ARGUMENT, "header is missing alg");
Expand All @@ -85,6 +92,19 @@ util::Status ValidateHeader(absl::string_view encoded_header,
return util::OkStatus();
}

absl::optional<std::string> GetTypeHeader(
const google::protobuf::Struct& header) {
auto it = header.fields().find("typ");
if (it == header.fields().end()) {
return absl::nullopt;
}
const auto& value = it->second;
if (value.kind_case() != google::protobuf::Value::kStringValue) {
return absl::nullopt;
}
return value.string_value();
}

std::string EncodePayload(absl::string_view json_payload) {
return absl::WebSafeBase64Escape(json_payload);
}
Expand Down
8 changes: 6 additions & 2 deletions cc/jwt/internal/jwt_format.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
#ifndef TINK_JWT_INTERNAL_JWT_FORMAT_H_
#define TINK_JWT_INTERNAL_JWT_FORMAT_H_

#include "google/protobuf/struct.pb.h"
#include "tink/util/status.h"
#include "tink/util/statusor.h"

Expand All @@ -27,9 +28,12 @@ namespace jwt_internal {
std::string EncodeHeader(absl::string_view json_header);
bool DecodeHeader(absl::string_view header, std::string* json_header);

std::string CreateHeader(absl::string_view algorithm);
util::Status ValidateHeader(absl::string_view encoded_header,
std::string CreateHeader(absl::string_view algorithm,
absl::optional<absl::string_view> type_header);
util::Status ValidateHeader(const google::protobuf::Struct& header,
absl::string_view algorithm);
absl::optional<std::string> GetTypeHeader(
const google::protobuf::Struct& header);

std::string EncodePayload(absl::string_view json_payload);
bool DecodePayload(absl::string_view payload, std::string* json_payload);
Expand Down
102 changes: 68 additions & 34 deletions cc/jwt/internal/jwt_format_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

#include "gmock/gmock.h"
#include "gtest/gtest.h"
#include "tink/jwt/internal/json_util.h"
#include "tink/util/test_matchers.h"
#include "tink/util/test_util.h"

Expand Down Expand Up @@ -79,80 +80,113 @@ TEST(JwtFormat, DecodeAndValidateFixedHeaderHS256) {
// Example from https://tools.ietf.org/html/rfc7515#appendix-A.1
std::string encoded_header = "eyJ0eXAiOiJKV1QiLA0KICJhbGciOiJIUzI1NiJ9";

std::string output;
ASSERT_TRUE(DecodeHeader(encoded_header, &output));
EXPECT_THAT(output, Eq("{\"typ\":\"JWT\",\r\n \"alg\":\"HS256\"}"));
std::string json_header;
ASSERT_TRUE(DecodeHeader(encoded_header, &json_header));
EXPECT_THAT(json_header, Eq("{\"typ\":\"JWT\",\r\n \"alg\":\"HS256\"}"));

util::StatusOr<google::protobuf::Struct> header_or =
JsonStringToProtoStruct(json_header);
EXPECT_THAT(header_or.status(), IsOk());

EXPECT_THAT(ValidateHeader(encoded_header, "HS256"), IsOk());
EXPECT_FALSE(ValidateHeader(encoded_header, "RS256").ok());
EXPECT_THAT(ValidateHeader(header_or.ValueOrDie(), "HS256"), IsOk());
EXPECT_FALSE(ValidateHeader(header_or.ValueOrDie(), "RS256").ok());
}

TEST(JwtFormat, DecodeAndValidateFixedHeaderRS256) {
// Example from https://tools.ietf.org/html/rfc7515#appendix-A.2
std::string encoded_header = "eyJhbGciOiJSUzI1NiJ9";

std::string output;
ASSERT_TRUE(DecodeHeader(encoded_header, &output));
EXPECT_THAT(output, Eq(R"({"alg":"RS256"})"));
std::string json_header;
ASSERT_TRUE(DecodeHeader(encoded_header, &json_header));
EXPECT_THAT(json_header, Eq(R"({"alg":"RS256"})"));

util::StatusOr<google::protobuf::Struct> header_or =
JsonStringToProtoStruct(json_header);
EXPECT_THAT(header_or.status(), IsOk());

EXPECT_THAT(ValidateHeader(encoded_header, "RS256"), IsOk());
EXPECT_FALSE(ValidateHeader(encoded_header, "HS256").ok());
EXPECT_THAT(ValidateHeader(header_or.ValueOrDie(), "RS256"), IsOk());
EXPECT_FALSE(ValidateHeader(header_or.ValueOrDie(), "HS256").ok());
}

TEST(JwtFormat, CreateValidateHeader) {
std::string encoded_header = CreateHeader("PS384");
EXPECT_THAT(ValidateHeader(encoded_header, "PS384"), IsOk());
EXPECT_FALSE(ValidateHeader(encoded_header, "HS256").ok());
}
std::string encoded_header = CreateHeader("PS384", absl::nullopt);

TEST(JwtFormat, ValidateEmptyHeaderFails) {
std::string header = "{}";
EXPECT_FALSE(ValidateHeader(EncodeHeader(header), "HS256").ok());
std::string json_header;
ASSERT_TRUE(DecodeHeader(encoded_header, &json_header));

util::StatusOr<google::protobuf::Struct> header_or =
JsonStringToProtoStruct(json_header);
EXPECT_THAT(header_or.status(), IsOk());

EXPECT_THAT(ValidateHeader(header_or.ValueOrDie(), "PS384"), IsOk());
EXPECT_FALSE(ValidateHeader(header_or.ValueOrDie(), "HS256").ok());
}

TEST(JwtFormat, ValidateInvalidEncodedHeaderFails) {
EXPECT_FALSE(
ValidateHeader("eyJ0eXAiOiJKV1Q?LA0KICJhbGciOiJIUzI1NiJ9", "HS256").ok());
TEST(JwtFormat, CreateValidateHeaderWithType) {
std::string encoded_header = CreateHeader("PS384", "JWT");

std::string json_header;
ASSERT_TRUE(DecodeHeader(encoded_header, &json_header));

util::StatusOr<google::protobuf::Struct> header_or =
JsonStringToProtoStruct(json_header);
EXPECT_THAT(header_or.status(), IsOk());

EXPECT_THAT(ValidateHeader(header_or.ValueOrDie(), "PS384"), IsOk());
EXPECT_FALSE(ValidateHeader(header_or.ValueOrDie(), "HS256").ok());
}

TEST(JwtFormat, ValidateInvalidJsonHeaderFails) {
std::string header = R"({"alg":"HS256")"; // missing }
EXPECT_FALSE(ValidateHeader(EncodeHeader(header), "HS256").ok());
TEST(JwtFormat, ValidateEmptyHeaderFails) {
google::protobuf::Struct empty_header;
EXPECT_FALSE(ValidateHeader(empty_header, "HS256").ok());
}

TEST(JwtFormat, ValidateHeaderIgnoresTyp) {
std::string header = R"({"alg":"HS256","typ":"unknown"})";
EXPECT_THAT(ValidateHeader(EncodeHeader(header), "HS256"), IsOk());
TEST(JwtFormat, ValidateHeaderWithUnknownTypeOk) {
std::string json_header = R"({"alg":"HS256","typ":"unknown"})";
util::StatusOr<google::protobuf::Struct> header_or =
JsonStringToProtoStruct(json_header);
EXPECT_THAT(header_or.status(), IsOk());

EXPECT_THAT(ValidateHeader(header_or.ValueOrDie(), "HS256"), IsOk());
}

TEST(JwtFormat, ValidateHeaderRejectsCrit) {
std::string header =
std::string json_header =
R"({"alg":"HS256","crit":["http://example.invalid/UNDEFINED"],)"
R"("http://example.invalid/UNDEFINED":true})";
EXPECT_FALSE(ValidateHeader(EncodeHeader(header), "HS256").ok());
util::StatusOr<google::protobuf::Struct> header_or =
JsonStringToProtoStruct(json_header);
EXPECT_THAT(header_or.status(), IsOk());
EXPECT_FALSE(ValidateHeader(header_or.ValueOrDie(), "HS256").ok());
}

TEST(JwtFormat, ValidateHeaderWithUnknownEntry) {
std::string header = R"({"alg":"HS256","unknown":"header"})";
EXPECT_THAT(ValidateHeader(EncodeHeader(header), "HS256"), IsOk());
std::string json_header = R"({"alg":"HS256","unknown":"header"})";
util::StatusOr<google::protobuf::Struct> header_or =
JsonStringToProtoStruct(json_header);
EXPECT_THAT(header_or.status(), IsOk());
EXPECT_THAT(ValidateHeader(header_or.ValueOrDie(), "HS256"), IsOk());
}

TEST(JwtFormat, ValidateHeaderWithInvalidAlgTypFails) {
std::string header = R"({"alg":true})";
EXPECT_FALSE(ValidateHeader(EncodeHeader(header), "HS256").ok());
std::string json_header = R"({"alg":true})";
util::StatusOr<google::protobuf::Struct> header_or =
JsonStringToProtoStruct(json_header);
EXPECT_THAT(header_or.status(), IsOk());
EXPECT_FALSE(ValidateHeader(header_or.ValueOrDie(), "HS256").ok());
}

TEST(JwtFormat, DecodeFixedPayload) {
// Example from https://tools.ietf.org/html/rfc7519#section-3.1
std::string encoded_header =
std::string encoded_payload =
"eyJpc3MiOiJqb2UiLA0KICJleHAiOjEzMDA4MTkzODAsDQogImh0"
"dHA6Ly9leGFtcGxlLmNvbS9pc19yb290Ijp0cnVlfQ";

std::string expected =
"{\"iss\":\"joe\",\r\n \"exp\":1300819380,\r\n "
"\"http://example.com/is_root\":true}";
std::string output;
ASSERT_TRUE(DecodePayload(encoded_header, &output));
ASSERT_TRUE(DecodePayload(encoded_payload, &output));
EXPECT_THAT(output, Eq(expected));
}

Expand Down
26 changes: 22 additions & 4 deletions cc/jwt/internal/jwt_mac_impl.cc
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,16 @@ namespace jwt_internal {

util::StatusOr<std::string> JwtMacImpl::ComputeMacAndEncode(
const RawJwt& token) const {
std::string encoded_header = CreateHeader(algorithm_);
util::StatusOr<std::string> payload_or = token.ToString();
absl::optional<std::string> type_header;
if (token.HasTypeHeader()) {
util::StatusOr<std::string> type_or = token.GetTypeHeader();
if (!type_or.ok()) {
return type_or.status();
}
type_header = type_or.ValueOrDie();
}
std::string encoded_header = CreateHeader(algorithm_, type_header);
util::StatusOr<std::string> payload_or = token.GetJsonPayload();
if (!payload_or.ok()) {
return payload_or.status();
}
Expand Down Expand Up @@ -64,15 +72,25 @@ util::StatusOr<VerifiedJwt> JwtMacImpl::VerifyMacAndDecode(
util::error::INVALID_ARGUMENT,
"only tokens in JWS compact serialization format are supported");
}
util::Status validate_header_result = ValidateHeader(parts[0], algorithm_);
std::string json_header;
if (!DecodeHeader(parts[0], &json_header)) {
return util::Status(util::error::INVALID_ARGUMENT, "invalid header");
}
auto header_or = JsonStringToProtoStruct(json_header);
if (!header_or.ok()) {
return header_or.status();
}
util::Status validate_header_result =
ValidateHeader(header_or.ValueOrDie(), algorithm_);
if (!validate_header_result.ok()) {
return validate_header_result;
}
std::string json_payload;
if (!DecodePayload(parts[1], &json_payload)) {
return util::Status(util::error::INVALID_ARGUMENT, "invalid JWT payload");
}
auto raw_jwt_or = RawJwt::FromString(json_payload);
auto raw_jwt_or =
RawJwt::FromJson(GetTypeHeader(header_or.ValueOrDie()), json_payload);
if (!raw_jwt_or.ok()) {
return raw_jwt_or.status();
}
Expand Down
13 changes: 9 additions & 4 deletions cc/jwt/internal/jwt_mac_impl_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
#include "tink/util/test_util.h"

using ::crypto::tink::test::IsOk;
using ::crypto::tink::test::IsOkAndHolds;

namespace crypto {
namespace tink {
Expand Down Expand Up @@ -67,13 +68,16 @@ TEST(JwtMacImplTest, CreateAndValidateToken) {
std::unique_ptr<JwtMac> jwt_mac = std::move(jwt_mac_or.ValueOrDie());

absl::Time now = absl::Now();
auto builder = RawJwtBuilder().SetIssuer("issuer");
auto builder =
RawJwtBuilder().SetTypeHeader("typeHeader").SetIssuer("issuer");
ASSERT_THAT(builder.SetNotBefore(now - absl::Seconds(300)), IsOk());
ASSERT_THAT(builder.SetIssuedAt(now), IsOk());
ASSERT_THAT(builder.SetExpiration(now + absl::Seconds(300)), IsOk());
auto raw_jwt_or = builder.Build();
ASSERT_THAT(raw_jwt_or.status(), IsOk());
RawJwt raw_jwt = raw_jwt_or.ValueOrDie();
EXPECT_TRUE(raw_jwt.HasTypeHeader());
EXPECT_THAT(raw_jwt.GetTypeHeader(), IsOkAndHolds("typeHeader"));

util::StatusOr<std::string> compact_or =
jwt_mac->ComputeMacAndEncode(raw_jwt);
Expand All @@ -86,7 +90,8 @@ TEST(JwtMacImplTest, CreateAndValidateToken) {
jwt_mac->VerifyMacAndDecode(compact, validator);
ASSERT_THAT(verified_jwt_or.status(), IsOk());
auto verified_jwt = verified_jwt_or.ValueOrDie();
EXPECT_THAT(verified_jwt.GetIssuer(), test::IsOkAndHolds("issuer"));
EXPECT_THAT(verified_jwt.GetTypeHeader(), IsOkAndHolds("typeHeader"));
EXPECT_THAT(verified_jwt.GetIssuer(), IsOkAndHolds("issuer"));

JwtValidator validator2 = JwtValidatorBuilder().SetIssuer("unknown").Build();
EXPECT_FALSE(jwt_mac->VerifyMacAndDecode(compact, validator2).ok());
Expand All @@ -110,9 +115,9 @@ TEST(JwtMacImplTest, ValidateFixedToken) {
jwt_mac->VerifyMacAndDecode(compact, validator_1970);
ASSERT_THAT(verified_jwt_or.status(), IsOk());
auto verified_jwt = verified_jwt_or.ValueOrDie();
EXPECT_THAT(verified_jwt.GetIssuer(), test::IsOkAndHolds("joe"));
EXPECT_THAT(verified_jwt.GetIssuer(), IsOkAndHolds("joe"));
EXPECT_THAT(verified_jwt.GetBooleanClaim("http://example.com/is_root"),
test::IsOkAndHolds(true));
IsOkAndHolds(true));

// verification fails because token is expired
JwtValidator validator_now = JwtValidatorBuilder().Build();
Expand Down
Loading

0 comments on commit 8b1b361

Please sign in to comment.