blob: c3655964cc25ac2ee6acfbc34cb77a2f52ac2b1a [file] [log] [blame]
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you 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.
//
// Copied from Impala and adapted to Kudu.
#include "kudu/util/jwt-util.h"
#include <chrono>
#include <functional>
#include <iostream>
#include <memory>
#include <string>
#include <thread>
#include <type_traits>
#include <unordered_map>
#include <utility>
#include <vector>
#include <glog/logging.h>
#include <gtest/gtest.h>
#include <jwt-cpp/jwt.h>
#include <jwt-cpp/traits/kazuho-picojson/defaults.h>
#include <jwt-cpp/traits/kazuho-picojson/traits.h>
#include "kudu/gutil/strings/substitute.h"
#include "kudu/server/webserver.h"
#include "kudu/server/webserver_options.h"
#include "kudu/util/countdown_latch.h"
#include "kudu/util/env.h"
#include "kudu/util/jwt-util-internal.h"
#include "kudu/util/jwt_test_certs.h"
#include "kudu/util/mini_oidc.h"
#include "kudu/util/net/sockaddr.h"
#include "kudu/util/scoped_cleanup.h"
#include "kudu/util/slice.h"
#include "kudu/util/status.h"
#include "kudu/util/test_macros.h"
#include "kudu/util/web_callback_registry.h"
namespace kudu {
using std::string;
using std::unique_ptr;
using std::unordered_map;
using std::vector;
using strings::Substitute;
using kudu::MiniOidc;
/// Utility class for creating a file that will be automatically deleted upon test
/// completion.
class TempTestDataFile {
public:
// Creates a temporary file with the specified contents.
explicit TempTestDataFile(const std::string& contents);
/// Returns the absolute path to the file.
const std::string& Filename() const { return name_; }
private:
std::string name_;
std::unique_ptr<WritableFile> tmp_file_;
};
TempTestDataFile::TempTestDataFile(const std::string& contents)
: name_("/tmp/jwks_XXXXXX") {
string created_filename;
WritableFileOptions opts;
opts.is_sensitive = false;
Status status;
status = Env::Default()->NewTempWritableFile(opts, &name_[0], &created_filename, &tmp_file_);
if (!status.ok()) {
std::cerr << Substitute("Error creating temp file: $0", created_filename);
}
status = WriteStringToFile(Env::Default(), contents, created_filename);
if (!status.ok()) {
std::cerr << Substitute("Error writing contents to temp file: $0", created_filename);
}
name_ = created_filename;
}
TEST(JwtUtilTest, LoadJwksFile) {
// Load JWKS from file.
TempTestDataFile jwks_file(Substitute(kJwksRsaFileFormat, kKid1, "RS256",
kRsaPubKeyJwkN, kRsaPubKeyJwkE, kKid2, "RS256", kRsaInvalidPubKeyJwkN,
kRsaPubKeyJwkE));
JWTHelper jwt_helper;
Status status = jwt_helper.Init(jwks_file.Filename());
EXPECT_OK(status);
JWKSSnapshotPtr jwks = jwt_helper.GetJWKS();
ASSERT_FALSE(jwks->IsEmpty());
ASSERT_EQ(2, jwks->GetRSAPublicKeyNum());
const JWTPublicKey* key1 = jwks->LookupRSAPublicKey(kKid1);
ASSERT_TRUE(key1 != nullptr);
ASSERT_EQ("rs256", key1->get_algorithm());
ASSERT_EQ(kRsaPubKeyPem, key1->get_key());
std::string non_existing_kid("public:c424b67b-fe28-45d7-b015-f79da5-xxxxx");
const JWTPublicKey* key3 = jwks->LookupRSAPublicKey(non_existing_kid);
ASSERT_FALSE(key3 != nullptr);
}
TEST(JwtUtilTest, LoadInvalidJwksFiles) {
// JWK without kid.
std::unique_ptr<TempTestDataFile> jwks_file(new TempTestDataFile(
"{"
" \"keys\": ["
" {"
" \"use\": \"sig\","
" \"kty\": \"RSA\","
" \"alg\": \"RS256\","
" \"n\": \"sttddbg-_yjXzcFpbMJB1fIFam9lQBeXWbTqzJwbuFbspHMsRowa8FaPw\","
" \"e\": \"AQAB\""
" }"
" ]"
"}"));
JWTHelper jwt_helper;
Status status = jwt_helper.Init(jwks_file->Filename());
ASSERT_FALSE(status.ok());
ASSERT_STR_CONTAINS(status.ToString(), "parsing key #0")
<< " Actual error: " << status.ToString();
ASSERT_STR_CONTAINS(status.ToString(), "'kid' property is required")
<< "actual error: " << status.ToString();
// Invalid JSON format, missing "]" and "}".
jwks_file.reset(new TempTestDataFile(
"{"
" \"keys\": ["
" {"
" \"use\": \"sig\","
" \"kty\": \"RSA\","
" \"kid\": \"public:c424b67b-fe28-45d7-b015-f79da50b5b21\","
" \"alg\": \"RS256\","
" \"n\": \"sttddbg-_yjXzcFpbMJB1fIFam9lQBeXWbTqzJwbuFbspHMsRowa8FaPw\","
" \"e\": \"AQAB\""
"}"));
status = jwt_helper.Init(jwks_file->Filename());
ASSERT_FALSE(status.ok());
ASSERT_STR_CONTAINS(status.ToString(), "Missing a comma or ']' after an array element")
<< " Actual error: " << status.ToString();
// JWKS with empty key id.
jwks_file.reset(new TempTestDataFile(
Substitute(kJwksRsaFileFormat, "", "RS256", kRsaPubKeyJwkN, kRsaPubKeyJwkE,
"", "RS256", kRsaInvalidPubKeyJwkN, kRsaPubKeyJwkE)));
status = jwt_helper.Init(jwks_file->Filename());
ASSERT_FALSE(status.ok());
ASSERT_STR_CONTAINS(status.ToString(), "parsing key #0")
<< " Actual error: " << status.ToString();
ASSERT_STR_CONTAINS(status.ToString(), "'kid' property must be a non-empty string")
<< " Actual error: " << status.ToString();
// JWKS with empty key value.
jwks_file.reset(new TempTestDataFile(
Substitute(kJwksRsaFileFormat, kKid1, "RS256", "", "", kKid2, "RS256", "", "")));
status = jwt_helper.Init(jwks_file->Filename());
ASSERT_FALSE(status.ok());
ASSERT_STR_CONTAINS(status.ToString(), "parsing key #0")
<< " Actual error: " << status.ToString();
ASSERT_STR_CONTAINS(status.ToString(), "'n' and 'e' properties must be a non-empty string")
<< " Actual error: " << status.ToString();
}
TEST(JwtUtilTest, VerifyJwtHS256) {
// Cryptographic algorithm: HS256.
// SharedSecret (Generated for MAC key (Base64 encoded)).
string shared_secret = "Yx57JSBzhGFDgDj19CabRpH/+kiaKqI6UZI6lDunQKw=";
TempTestDataFile jwks_file(
Substitute(kJwksHsFileFormat, kKid1, "HS256", shared_secret));
JWTHelper jwt_helper;
Status status = jwt_helper.Init(jwks_file.Filename());
EXPECT_OK(status);
JWKSSnapshotPtr jwks = jwt_helper.GetJWKS();
EXPECT_OK(status);
ASSERT_EQ(1, jwks->GetHSKeyNum());
const JWTPublicKey* key1 = jwks->LookupHSKey(kKid1);
ASSERT_TRUE(key1 != nullptr);
ASSERT_EQ(key1->get_key(), shared_secret);
// Create a JWT token and sign it with HS256.
auto token = jwt::create()
.set_issuer("auth0")
.set_type("JWS")
.set_algorithm("HS256")
.set_key_id(kKid1)
.set_payload_claim("username", picojson::value("kudu"))
.sign(jwt::algorithm::hs256(shared_secret));
// Verify the JWT token with our wrapper class which use public key retrieved from JWKS,
// and read username from the token.
JWTHelper::UniqueJWTDecodedToken decoded_token;
status = JWTHelper::Decode(token, decoded_token);
EXPECT_OK(status);
status = jwt_helper.Verify(decoded_token.get());
EXPECT_OK(status);
string username;
status = JWTHelper::GetCustomClaimUsername(decoded_token.get(), "username", username);
EXPECT_OK(status);
ASSERT_EQ("kudu", username);
}
TEST(JwtUtilTest, VerifyJwtHS384) {
// Cryptographic algorithm: HS384.
// SharedSecret (Generated for MAC key (Base64 encoded)).
string shared_secret =
"TlqmKRc2PNQJXTC3Go7eAadwPxA7x9byyXCi5I8tSvxrE77tYbuF5pfZAyswrkou";
TempTestDataFile jwks_file(
Substitute(kJwksHsFileFormat, kKid1, "HS384", shared_secret));
JWTHelper jwt_helper;
Status status = jwt_helper.Init(jwks_file.Filename());
EXPECT_OK(status);
JWKSSnapshotPtr jwks = jwt_helper.GetJWKS();
ASSERT_EQ(1, jwks->GetHSKeyNum());
const JWTPublicKey* key1 = jwks->LookupHSKey(kKid1);
ASSERT_TRUE(key1 != nullptr);
ASSERT_EQ(key1->get_key(), shared_secret);
// Create a JWT token and sign it with HS384.
auto token = jwt::create()
.set_issuer("auth0")
.set_type("JWS")
.set_algorithm("HS384")
.set_key_id(kKid1)
.set_payload_claim("username", picojson::value("kudu"))
.sign(jwt::algorithm::hs384(shared_secret));
// Verify the JWT token with our wrapper class which use public key retrieved from JWKS,
// and read username from the token.
JWTHelper::UniqueJWTDecodedToken decoded_token;
status = JWTHelper::Decode(token, decoded_token);
EXPECT_OK(status);
status = jwt_helper.Verify(decoded_token.get());
EXPECT_OK(status);
string username;
status = JWTHelper::GetCustomClaimUsername(decoded_token.get(), "username", username);
EXPECT_OK(status);
ASSERT_EQ("kudu", username);
}
TEST(JwtUtilTest, VerifyJwtHS512) {
// Cryptographic algorithm: HS512.
// SharedSecret (Generated for MAC key (Base64 encoded)).
string shared_secret = "ywc6DN7+iRw1E5HOqzvrsYodykSLFutT28KN3bJnLZcZpPCNjn0b6gbMfXPcxeY"
"VyuWWGDxh6gCDwPMejbuEEg==";
TempTestDataFile jwks_file(
Substitute(kJwksHsFileFormat, kKid1, "HS512", shared_secret));
JWTHelper jwt_helper;
Status status = jwt_helper.Init(jwks_file.Filename());
EXPECT_OK(status);
JWKSSnapshotPtr jwks = jwt_helper.GetJWKS();
EXPECT_OK(status);
ASSERT_EQ(1, jwks->GetHSKeyNum());
const JWTPublicKey* key1 = jwks->LookupHSKey(kKid1);
ASSERT_TRUE(key1 != nullptr);
ASSERT_EQ(key1->get_key(), shared_secret);
// Create a JWT token and sign it with HS512.
auto token = jwt::create()
.set_issuer("auth0")
.set_type("JWS")
.set_algorithm("HS512")
.set_key_id(kKid1)
.set_payload_claim("username", picojson::value("kudu"))
.sign(jwt::algorithm::hs512(shared_secret));
// Verify the JWT token with our wrapper class which use public key retrieved from JWKS,
// and read username from the token.
JWTHelper::UniqueJWTDecodedToken decoded_token;
status = JWTHelper::Decode(token, decoded_token);
EXPECT_OK(status);
status = jwt_helper.Verify(decoded_token.get());
EXPECT_OK(status);
string username;
status = JWTHelper::GetCustomClaimUsername(decoded_token.get(), "username", username);
EXPECT_OK(status);
ASSERT_EQ("kudu", username);
}
TEST(JwtUtilTest, VerifyJwtRS256) {
// Cryptographic algorithm: RS256.
TempTestDataFile jwks_file(Substitute(kJwksRsaFileFormat, kKid1, "RS256",
kRsaPubKeyJwkN, kRsaPubKeyJwkE, kKid2, "RS256", kRsaInvalidPubKeyJwkN,
kRsaPubKeyJwkE));
JWTHelper jwt_helper;
Status status = jwt_helper.Init(jwks_file.Filename());
EXPECT_OK(status);
JWKSSnapshotPtr jwks = jwt_helper.GetJWKS();
ASSERT_EQ(2, jwks->GetRSAPublicKeyNum());
const JWTPublicKey* key1 = jwks->LookupRSAPublicKey(kKid1);
ASSERT_TRUE(key1 != nullptr);
ASSERT_EQ(kRsaPubKeyPem, key1->get_key());
// Create a JWT token and sign it with RS256.
auto token =
jwt::create()
.set_issuer("auth0")
.set_type("JWS")
.set_algorithm("RS256")
.set_key_id(kKid1)
.set_payload_claim("username", picojson::value("kudu"))
.sign(jwt::algorithm::rs256(kRsaPubKeyPem, kRsaPrivKeyPem, "", ""));
ASSERT_EQ(
"eyJhbGciOiJSUzI1NiIsImtpZCI6InB1YmxpYzpjNDI0YjY3Yi1mZTI4LTQ1ZDctYjAxNS1mNzlkYTUwYjViMjEiLCJ0"
"eXAiOiJKV1MifQ.eyJpc3MiOiJhdXRoMCIsInVzZXJuYW1lIjoia3VkdSJ9.OcAO1KFnYMyVyMXWqeWKlvZIDIYmeQYZ"
"hMYcjOuGi5KKuBF5J7IlSl14EM6EhswQJ54pP8EPhMyHHNqncUOPt-QN9foS39aA5XpNPqOOWShQvZLQweEsogjfab66"
"gWPO7baXGg8npqMBxpnvM5mjz4TIg6kwT4R_9p1NBGYmU5DQS6_jb7OfpMxY8bezKwL_iJB9yPgTlgZA5IJ0DPkIydcQ"
"ejz3ycLy-75G8GWK78WgtOq2ejwpCsrPo3QlaqQH1reDPBit_2xme8ypwgGztc3Nss1ZF8g5U69WTdhP2Dy5k7iFXKua"
"PHD5HBAFJiP1KVWMpuGX_POewU_ibt7v8g",
token);
// Verify the JWT token with jwt-cpp APIs directly.
auto jwt_decoded_token = jwt::decode(token);
auto verifier = jwt::verify()
.allow_algorithm(jwt::algorithm::rs256(kRsaPubKeyPem, "", "", ""))
.with_issuer("auth0");
verifier.verify(jwt_decoded_token);
// Verify the JWT token with our wrapper class which use public key retrieved from JWKS,
// and read username from the token.
JWTHelper::UniqueJWTDecodedToken decoded_token;
status = JWTHelper::Decode(token, decoded_token);
EXPECT_OK(status);
status = jwt_helper.Verify(decoded_token.get());
EXPECT_OK(status);
string username;
status = JWTHelper::GetCustomClaimUsername(decoded_token.get(), "username", username);
EXPECT_OK(status);
ASSERT_EQ("kudu", username);
}
TEST(JwtUtilTest, VerifyJwtRS384) {
// Cryptographic algorithm: RS384.
TempTestDataFile jwks_file(Substitute(kJwksRsaFileFormat, kKid1, "RS384",
kRsaPubKeyJwkN, kRsaPubKeyJwkE, kKid2, "RS384", kRsaInvalidPubKeyJwkN,
kRsaPubKeyJwkE));
JWTHelper jwt_helper;
Status status = jwt_helper.Init(jwks_file.Filename());
EXPECT_OK(status);
JWKSSnapshotPtr jwks = jwt_helper.GetJWKS();
ASSERT_EQ(2, jwks->GetRSAPublicKeyNum());
const JWTPublicKey* key1 = jwks->LookupRSAPublicKey(kKid1);
ASSERT_TRUE(key1 != nullptr);
ASSERT_EQ(kRsaPubKeyPem, key1->get_key());
// Create a JWT token and sign it with RS384.
auto token =
jwt::create()
.set_issuer("auth0")
.set_type("JWS")
.set_algorithm("RS384")
.set_key_id(kKid1)
.set_payload_claim("username", picojson::value("kudu"))
.sign(jwt::algorithm::rs384(kRsaPubKeyPem, kRsaPrivKeyPem, "", ""));
// Verify the JWT token with our wrapper class which use public key retrieved from JWKS,
// and read username from the token.
JWTHelper::UniqueJWTDecodedToken decoded_token;
status = JWTHelper::Decode(token, decoded_token);
EXPECT_OK(status);
status = jwt_helper.Verify(decoded_token.get());
EXPECT_OK(status);
string username;
status = JWTHelper::GetCustomClaimUsername(decoded_token.get(), "username", username);
EXPECT_OK(status);
ASSERT_EQ("kudu", username);
}
TEST(JwtUtilTest, VerifyJwtRS512) {
// Cryptographic algorithm: RS512.
TempTestDataFile jwks_file(Substitute(kJwksRsaFileFormat, kKid1, "RS512",
kRsa512PubKeyJwkN, kRsa512PubKeyJwkE, kKid2, "RS512",
kRsa512InvalidPubKeyJwkN, kRsa512PubKeyJwkE));
JWTHelper jwt_helper;
Status status = jwt_helper.Init(jwks_file.Filename());
EXPECT_OK(status);
JWKSSnapshotPtr jwks = jwt_helper.GetJWKS();
ASSERT_EQ(2, jwks->GetRSAPublicKeyNum());
const JWTPublicKey* key1 = jwks->LookupRSAPublicKey(kKid1);
ASSERT_TRUE(key1 != nullptr);
ASSERT_EQ(kRsa512PubKeyPem, key1->get_key());
// Create a JWT token and sign it with RS512.
auto token =
jwt::create()
.set_issuer("auth0")
.set_type("JWS")
.set_algorithm("RS512")
.set_key_id(kKid1)
.set_payload_claim("username", picojson::value("kudu"))
.sign(jwt::algorithm::rs512(kRsa512PubKeyPem, kRsa512PrivKeyPem, "", ""));
// Verify the JWT token with our wrapper class which use public key retrieved from JWKS,
// and read username from the token.
JWTHelper::UniqueJWTDecodedToken decoded_token;
status = JWTHelper::Decode(token, decoded_token);
EXPECT_OK(status);
status = jwt_helper.Verify(decoded_token.get());
EXPECT_OK(status);
string username;
status = JWTHelper::GetCustomClaimUsername(decoded_token.get(), "username", username);
EXPECT_OK(status);
ASSERT_EQ("kudu", username);
}
TEST(JwtUtilTest, VerifyJwtPS256) {
// Cryptographic algorithm: PS256.
TempTestDataFile jwks_file(Substitute(kJwksRsaFileFormat, kKid1, "PS256",
kRsa1024PubKeyJwkN, kRsa1024PubKeyJwkE, kKid2, "PS256",
kRsaInvalidPubKeyJwkN, kRsaPubKeyJwkE));
JWTHelper jwt_helper;
Status status = jwt_helper.Init(jwks_file.Filename());
EXPECT_OK(status);
JWKSSnapshotPtr jwks = jwt_helper.GetJWKS();
ASSERT_EQ(2, jwks->GetRSAPublicKeyNum());
const JWTPublicKey* key1 = jwks->LookupRSAPublicKey(kKid1);
ASSERT_TRUE(key1 != nullptr);
ASSERT_EQ(kRsa1024PubKeyPem, key1->get_key());
// Create a JWT token and sign it with PS256.
auto token =
jwt::create()
.set_issuer("auth0")
.set_type("JWS")
.set_algorithm("PS256")
.set_key_id(kKid1)
.set_payload_claim("username", picojson::value("kudu"))
.sign(jwt::algorithm::ps256(kRsa1024PubKeyPem, kRsa1024PrivKeyPem, "", ""));
// Verify the JWT token with our wrapper class which use public key retrieved from JWKS,
// and read username from the token.
JWTHelper::UniqueJWTDecodedToken decoded_token;
status = JWTHelper::Decode(token, decoded_token);
EXPECT_OK(status);
status = jwt_helper.Verify(decoded_token.get());
EXPECT_OK(status);
string username;
status = JWTHelper::GetCustomClaimUsername(decoded_token.get(), "username", username);
EXPECT_OK(status);
ASSERT_EQ("kudu", username);
}
TEST(JwtUtilTest, VerifyJwtPS384) {
// Cryptographic algorithm: PS384.
TempTestDataFile jwks_file(Substitute(kJwksRsaFileFormat, kKid1, "PS384",
kRsa2048PubKeyJwkN, kRsa2048PubKeyJwkE, kKid2, "PS384",
kRsaInvalidPubKeyJwkN, kRsaPubKeyJwkE));
JWTHelper jwt_helper;
Status status = jwt_helper.Init(jwks_file.Filename());
EXPECT_OK(status);
JWKSSnapshotPtr jwks = jwt_helper.GetJWKS();
ASSERT_EQ(2, jwks->GetRSAPublicKeyNum());
const JWTPublicKey* key1 = jwks->LookupRSAPublicKey(kKid1);
ASSERT_TRUE(key1 != nullptr);
ASSERT_EQ(kRsa2048PubKeyPem, key1->get_key());
// Create a JWT token and sign it with PS384.
auto token =
jwt::create()
.set_issuer("auth0")
.set_type("JWS")
.set_algorithm("PS384")
.set_key_id(kKid1)
.set_payload_claim("username", picojson::value("kudu"))
.sign(jwt::algorithm::ps384(kRsa2048PubKeyPem, kRsa2048PrivKeyPem, "", ""));
// Verify the JWT token with our wrapper class which use public key retrieved from JWKS,
// and read username from the token.
JWTHelper::UniqueJWTDecodedToken decoded_token;
status = JWTHelper::Decode(token, decoded_token);
EXPECT_OK(status);
status = jwt_helper.Verify(decoded_token.get());
EXPECT_OK(status);
string username;
status = JWTHelper::GetCustomClaimUsername(decoded_token.get(), "username", username);
EXPECT_OK(status);
ASSERT_EQ("kudu", username);
}
TEST(JwtUtilTest, VerifyJwtPS512) {
// Cryptographic algorithm: PS512.
TempTestDataFile jwks_file(Substitute(kJwksRsaFileFormat, kKid1, "PS512",
kRsa4096PubKeyJwkN, kRsa4096PubKeyJwkE, kKid2, "PS512",
kRsaInvalidPubKeyJwkN, kRsaPubKeyJwkE));
JWTHelper jwt_helper;
Status status = jwt_helper.Init(jwks_file.Filename());
EXPECT_OK(status);
JWKSSnapshotPtr jwks = jwt_helper.GetJWKS();
ASSERT_EQ(2, jwks->GetRSAPublicKeyNum());
const JWTPublicKey* key1 = jwks->LookupRSAPublicKey(kKid1);
ASSERT_TRUE(key1 != nullptr);
ASSERT_EQ(kRsa4096PubKeyPem, key1->get_key());
// Create a JWT token and sign it with PS512.
auto token =
jwt::create()
.set_issuer("auth0")
.set_type("JWS")
.set_algorithm("PS512")
.set_key_id(kKid1)
.set_payload_claim("username", picojson::value("kudu"))
.sign(jwt::algorithm::ps512(kRsa4096PubKeyPem, kRsa4096PrivKeyPem, "", ""));
// Verify the JWT token with our wrapper class which use public key retrieved from JWKS,
// and read username from the token.
JWTHelper::UniqueJWTDecodedToken decoded_token;
status = JWTHelper::Decode(token, decoded_token);
EXPECT_OK(status);
status = jwt_helper.Verify(decoded_token.get());
EXPECT_OK(status);
string username;
status = JWTHelper::GetCustomClaimUsername(decoded_token.get(), "username", username);
EXPECT_OK(status);
ASSERT_EQ("kudu", username);
}
TEST(JwtUtilTest, VerifyJwtES256) {
// Cryptographic algorithm: ES256.
TempTestDataFile jwks_file(Substitute(kJwksEcFileFormat, kKid1, "P-256",
kEcdsa256PubKeyJwkX, kEcdsa256PubKeyJwkY));
JWTHelper jwt_helper;
Status status = jwt_helper.Init(jwks_file.Filename());
EXPECT_OK(status);
JWKSSnapshotPtr jwks = jwt_helper.GetJWKS();
ASSERT_EQ(1, jwks->GetECPublicKeyNum());
const JWTPublicKey* key1 = jwks->LookupECPublicKey(kKid1);
ASSERT_TRUE(key1 != nullptr);
ASSERT_EQ(kEcdsa256PubKeyPem, key1->get_key());
// Create a JWT token and sign it with ES256.
auto token = jwt::create()
.set_issuer("auth0")
.set_type("JWS")
.set_algorithm("ES256")
.set_key_id(kKid1)
.set_payload_claim("username", picojson::value("kudu"))
.sign(jwt::algorithm::es256(
kEcdsa256PubKeyPem, kEcdsa256PrivKeyPem, "", ""));
// Verify the JWT token with jwt-cpp APIs directly.
auto jwt_decoded_token = jwt::decode(token);
auto verifier =
jwt::verify()
.allow_algorithm(jwt::algorithm::es256(kEcdsa256PubKeyPem, "", "", ""))
.with_issuer("auth0");
verifier.verify(jwt_decoded_token);
// Verify the JWT token with our wrapper class which use public key retrieved from JWKS,
// and read username from the token.
JWTHelper::UniqueJWTDecodedToken decoded_token;
status = JWTHelper::Decode(token, decoded_token);
EXPECT_OK(status);
status = jwt_helper.Verify(decoded_token.get());
EXPECT_OK(status);
string username;
status = JWTHelper::GetCustomClaimUsername(decoded_token.get(), "username", username);
EXPECT_OK(status);
ASSERT_EQ("kudu", username);
}
TEST(JwtUtilTest, VerifyJwtES384) {
// Cryptographic algorithm: ES384.
TempTestDataFile jwks_file(Substitute(kJwksEcFileFormat, kKid1, "P-384",
kEcdsa384PubKeyJwkX, kEcdsa384PubKeyJwkY));
JWTHelper jwt_helper;
Status status = jwt_helper.Init(jwks_file.Filename());
EXPECT_OK(status);
JWKSSnapshotPtr jwks = jwt_helper.GetJWKS();
ASSERT_EQ(1, jwks->GetECPublicKeyNum());
const JWTPublicKey* key1 = jwks->LookupECPublicKey(kKid1);
ASSERT_TRUE(key1 != nullptr);
ASSERT_EQ(kEcdsa384PubKeyPem, key1->get_key());
// Create a JWT token and sign it with ES384.
auto token = jwt::create()
.set_issuer("auth0")
.set_type("JWS")
.set_algorithm("ES384")
.set_key_id(kKid1)
.set_payload_claim("username", picojson::value("kudu"))
.sign(jwt::algorithm::es384(
kEcdsa384PubKeyPem, kEcdsa384PrivKeyPem, "", ""));
// Verify the JWT token with our wrapper class which use public key retrieved from JWKS,
// and read username from the token.
JWTHelper::UniqueJWTDecodedToken decoded_token;
status = JWTHelper::Decode(token, decoded_token);
EXPECT_OK(status);
status = jwt_helper.Verify(decoded_token.get());
EXPECT_OK(status);
string username;
status = JWTHelper::GetCustomClaimUsername(decoded_token.get(), "username", username);
EXPECT_OK(status);
ASSERT_EQ("kudu", username);
}
TEST(JwtUtilTest, VerifyJwtES512) {
// Cryptographic algorithm: ES512.
TempTestDataFile jwks_file(Substitute(kJwksEcFileFormat, kKid1, "P-521",
kEcdsa521PubKeyJwkX, kEcdsa521PubKeyJwkY));
JWTHelper jwt_helper;
Status status = jwt_helper.Init(jwks_file.Filename());
EXPECT_OK(status);
JWKSSnapshotPtr jwks = jwt_helper.GetJWKS();
ASSERT_EQ(1, jwks->GetECPublicKeyNum());
const JWTPublicKey* key1 = jwks->LookupECPublicKey(kKid1);
ASSERT_TRUE(key1 != nullptr);
ASSERT_EQ(kEcdsa521PubKeyPem, key1->get_key());
// Create a JWT token and sign it with ES512.
auto token = jwt::create()
.set_issuer("auth0")
.set_type("JWS")
.set_algorithm("ES512")
.set_key_id(kKid1)
.set_payload_claim("username", picojson::value("kudu"))
.sign(jwt::algorithm::es512(
kEcdsa521PubKeyPem, kEcdsa521PrivKeyPem, "", ""));
// Verify the JWT token with our wrapper class which use public key retrieved from JWKS,
// and read username from the token.
JWTHelper::UniqueJWTDecodedToken decoded_token;
status = JWTHelper::Decode(token, decoded_token);
EXPECT_OK(status);
status = jwt_helper.Verify(decoded_token.get());
EXPECT_OK(status);
string username;
status = JWTHelper::GetCustomClaimUsername(decoded_token.get(), "username", username);
EXPECT_OK(status);
ASSERT_EQ("kudu", username);
}
TEST(JwtUtilTest, VerifyJwtNotVerifySignature) {
// Create a JWT token and sign it with RS256.
auto token =
jwt::create()
.set_issuer("auth0")
.set_type("JWS")
.set_algorithm("RS256")
.set_payload_claim("username", picojson::value("kudu"))
.sign(jwt::algorithm::rs256(kRsaPubKeyPem, kRsaPrivKeyPem, "", ""));
// Do not verify signature.
JWTHelper::UniqueJWTDecodedToken decoded_token;
Status status = JWTHelper::Decode(token, decoded_token);
EXPECT_OK(status);
string username;
status = JWTHelper::GetCustomClaimUsername(decoded_token.get(), "username", username);
EXPECT_OK(status);
ASSERT_EQ("kudu", username);
}
TEST(JwtUtilTest, VerifyJwtFailMismatchingAlgorithms) {
// JWT algorithm is not matching with algorithm in JWK.
TempTestDataFile jwks_file(Substitute(kJwksRsaFileFormat, kKid1, "RS256",
kRsaPubKeyJwkN, kRsaPubKeyJwkE, kKid2, "RS256", kRsaInvalidPubKeyJwkN,
kRsaPubKeyJwkE));
JWTHelper jwt_helper;
Status status = jwt_helper.Init(jwks_file.Filename());
EXPECT_OK(status);
// Create a JWT token, but set mismatching algorithm.
auto token =
jwt::create()
.set_issuer("auth0")
.set_type("JWS")
.set_algorithm("RS512")
.set_key_id(kKid1)
.sign(jwt::algorithm::rs256(kRsaPubKeyPem, kRsaPrivKeyPem, "", ""));
// Failed to verify the token due to mismatching algorithms.
JWTHelper::UniqueJWTDecodedToken decoded_token;
status = JWTHelper::Decode(token, decoded_token);
EXPECT_OK(status);
status = jwt_helper.Verify(decoded_token.get());
ASSERT_TRUE(status.IsNotAuthorized()) << status.ToString();
ASSERT_STR_CONTAINS(status.ToString(),
"JWT algorithm 'rs512' is not matching with JWK algorithm 'rs256'");
}
TEST(JwtUtilTest, VerifyJwtFailKeyNotFound) {
// The key cannot be found in JWKS.
TempTestDataFile jwks_file(Substitute(kJwksRsaFileFormat, kKid1, "RS256",
kRsaPubKeyJwkN, kRsaPubKeyJwkE, kKid2, "RS256", kRsaInvalidPubKeyJwkN,
kRsaPubKeyJwkE));
JWTHelper jwt_helper;
Status status = jwt_helper.Init(jwks_file.Filename());
EXPECT_OK(status);
// Create a JWT token with a key ID which can not be found in JWKS.
auto token =
jwt::create()
.set_issuer("auth0")
.set_type("JWS")
.set_algorithm("RS256")
.set_key_id("unfound-key-id")
.sign(jwt::algorithm::rs256(kRsaPubKeyPem, kRsaPrivKeyPem, "", ""));
// Failed to verify the token since key is not found in JWKS.
JWTHelper::UniqueJWTDecodedToken decoded_token;
status = JWTHelper::Decode(token, decoded_token);
EXPECT_OK(status);
status = jwt_helper.Verify(decoded_token.get());
ASSERT_TRUE(status.IsNotAuthorized()) << status.ToString();
ASSERT_STR_CONTAINS(status.ToString(), "Invalid JWK ID in the JWT token");
}
TEST(JwtUtilTest, VerifyJwtTokenWithoutKeyId) {
// Verify JWT token without key ID.
TempTestDataFile jwks_file(Substitute(kJwksRsaFileFormat, kKid1, "RS256",
kRsaPubKeyJwkN, kRsaPubKeyJwkE, kKid2, "RS256", kRsaInvalidPubKeyJwkN,
kRsaPubKeyJwkE));
JWTHelper jwt_helper;
Status status = jwt_helper.Init(jwks_file.Filename());
EXPECT_OK(status);
// Create a JWT token without key ID.
auto token =
jwt::create()
.set_issuer("auth0")
.set_type("JWS")
.set_algorithm("RS256")
.sign(jwt::algorithm::rs256(kRsaPubKeyPem, kRsaPrivKeyPem, "", ""));
// Verify the token by trying each key in JWK set and there is one matched key.
JWTHelper::UniqueJWTDecodedToken decoded_token;
status = JWTHelper::Decode(token, decoded_token);
EXPECT_OK(status);
status = jwt_helper.Verify(decoded_token.get());
EXPECT_OK(status);
}
TEST(JwtUtilTest, VerifyJwtFailTokenWithoutKeyId) {
// Verify JWT token without key ID.
TempTestDataFile jwks_file(Substitute(kJwksRsaFileFormat, kKid1, "RS256",
kRsaPubKeyJwkN, kRsaPubKeyJwkE, kKid2, "RS256", kRsaInvalidPubKeyJwkN,
kRsaPubKeyJwkE));
JWTHelper jwt_helper;
Status status = jwt_helper.Init(jwks_file.Filename());
EXPECT_OK(status);
// Create a JWT token without key ID.
auto token =
jwt::create()
.set_issuer("auth0")
.set_type("JWS")
.set_algorithm("RS512")
.sign(jwt::algorithm::rs512(kRsa512PubKeyPem, kRsa512PrivKeyPem, "", ""));
// Verify the token by trying each key in JWK set, but there is no matched key.
JWTHelper::UniqueJWTDecodedToken decoded_token;
status = JWTHelper::Decode(token, decoded_token);
EXPECT_OK(status);
status = jwt_helper.Verify(decoded_token.get());
ASSERT_FALSE(status.ok());
}
TEST(JwtUtilTest, VerifyJwtFailTokenWithoutSignature) {
TempTestDataFile jwks_file(Substitute(kJwksRsaFileFormat, kKid1, "RS256",
kRsaPubKeyJwkN, kRsaPubKeyJwkE, kKid2, "RS256", kRsaInvalidPubKeyJwkN,
kRsaPubKeyJwkE));
JWTHelper jwt_helper;
Status status = jwt_helper.Init(jwks_file.Filename());
EXPECT_OK(status);
// Create a JWT token without signature.
auto token =
jwt::create().set_issuer("auth0").set_type("JWS").sign(jwt::algorithm::none{});
// Failed to verify the unsigned token.
JWTHelper::UniqueJWTDecodedToken decoded_token;
status = JWTHelper::Decode(token, decoded_token);
EXPECT_OK(status);
status = jwt_helper.Verify(decoded_token.get());
ASSERT_TRUE(status.IsNotAuthorized()) << status.ToString();
ASSERT_STR_CONTAINS(status.ToString(), "Unsecured JWT");
}
TEST(JwtUtilTest, VerifyJwtFailExpiredToken) {
// Sign JWT token with RS256.
TempTestDataFile jwks_file(Substitute(kJwksRsaFileFormat, kKid1, "RS256",
kRsaPubKeyJwkN, kRsaPubKeyJwkE, kKid2, "RS256", kRsaInvalidPubKeyJwkN,
kRsaPubKeyJwkE));
JWTHelper jwt_helper;
Status status = jwt_helper.Init(jwks_file.Filename());
EXPECT_OK(status);
// Create a JWT token and sign it with RS256.
auto token =
jwt::create()
.set_issuer("auth0")
.set_type("JWS")
.set_algorithm("RS256")
.set_key_id(kKid1)
.set_issued_at(std::chrono::system_clock::now())
.set_expires_at(std::chrono::system_clock::now() - std::chrono::seconds{10})
.set_payload_claim("username", picojson::value("kudu"))
.sign(jwt::algorithm::rs256(kRsaPubKeyPem, kRsaPrivKeyPem, "", ""));
// Verify the token, including expiring time.
JWTHelper::UniqueJWTDecodedToken decoded_token;
status = JWTHelper::Decode(token, decoded_token);
EXPECT_OK(status);
status = jwt_helper.Verify(decoded_token.get());
ASSERT_TRUE(status.IsNotAuthorized()) << status.ToString();
ASSERT_STR_CONTAINS(status.ToString(), "JWT verification failed: token expired");
}
namespace {
// Returns a simple JWKS to be used by tokens signed by 'rsa_pub_key_pem' and
// 'rsa_priv_key_pem'.
void SimpleJWKSHandler(const Webserver::WebRequest& /*req*/,
Webserver::PrerenderedWebResponse* resp) {
resp->output <<
Substitute(kJwksRsaFileFormat, kKid1, "RS256",
kRsaPubKeyJwkN, kRsaPubKeyJwkE, kKid2, "RS256", kRsaInvalidPubKeyJwkN,
kRsaPubKeyJwkE);
resp->status_code = HttpStatusCode::Ok;
}
class JWKSMockServer {
public:
// Registers a path handler for a single JWKS to be used by tokens signed by
// 'rsa_pub_key_pem' and 'rsa_priv_key_pem'.
Status Start() {
WebserverOptions opts;
opts.port = 0;
opts.bind_interface = "127.0.0.1";
webserver_.reset(new Webserver(opts));
webserver_->RegisterPrerenderedPathHandler("/jwks", "JWKS", SimpleJWKSHandler,
/*is_styled*/false, /*is_on_nav_bar*/false);
RETURN_NOT_OK(webserver_->Start());
vector<Sockaddr> addrs;
RETURN_NOT_OK(webserver_->GetBoundAddresses(&addrs));
url_ = Substitute("http://$0/jwks", addrs[0].ToString());
return Status::OK();
}
// Register a path handler for every account ID in 'account_id_to_resp' that
// returns the correspodning HTTP response.
Status StartWithAccounts(const unordered_map<string, string>& account_id_to_resp) {
DCHECK(!webserver_);
WebserverOptions opts;
opts.port = 0;
opts.bind_interface = "127.0.0.1";
webserver_.reset(new Webserver(opts));
for (const auto& ar : account_id_to_resp) {
const auto& account_id = ar.first;
const auto& jwks = ar.second;
webserver_->RegisterPrerenderedPathHandler(Substitute("/jwks/$0", account_id), account_id,
[account_id, jwks] (const Webserver::WebRequest& /*req*/,
Webserver::PrerenderedWebResponse* resp) {
resp->output << jwks;
resp->status_code = HttpStatusCode::Ok;
},
/*is_styled*/false, /*is_on_nav_bar*/false);
}
RETURN_NOT_OK(webserver_->Start());
vector<Sockaddr> addrs;
RETURN_NOT_OK(webserver_->GetBoundAddresses(&addrs));
url_ = Substitute("http://$0/jwks", addrs[0].ToString());
return Status::OK();
}
const string& url() const {
return url_;
}
string url_for_account(const string& account_id) const {
return Substitute("$0/$1", url_, account_id);
}
private:
unique_ptr<Webserver> webserver_;
string url_;
};
} // anonymous namespace
TEST(JwtUtilTest, VerifyJWKSUrl) {
JWKSMockServer jwks_server;
ASSERT_OK(jwks_server.Start());
auto encoded_token =
jwt::create()
.set_issuer("auth0")
.set_type("JWT")
.set_algorithm("RS256")
.set_key_id(kKid1)
.set_subject("kudu")
.sign(jwt::algorithm::rs256(kRsaPubKeyPem, kRsaPrivKeyPem, "", ""));
ASSERT_EQ(
"eyJhbGciOiJSUzI1NiIsImtpZCI6InB1YmxpYzpjNDI0YjY3Yi1mZTI4LTQ1ZDctYjAxNS1mNzlkYTUwYjViMjEiLCJ0"
"eXAiOiJKV1QifQ.eyJpc3MiOiJhdXRoMCIsInN1YiI6Imt1ZHUifQ.VENjfXICRV1lr2M-jBElI_qaBZNFILjkXr1Amg"
"poH8xlI41EFN8RVkihuJtFijUOEFxJ537LCEonDBHsouO9iQlrxh0AobIjB1QraqG1BoQLnKWF78E-rhPN2K1aueGed7"
"A86lkIEB4s7VU9dSDtR3bwbP5RFaf3XRZ6TyVh0h5sdMo91YKpS6nLCvYh2OSIbsUJCSNu4BoCmDz97Wq1xLiDoRfAJh"
"BZiHQeHO38ydRMWIeto78pV2s9sf1CdwVwycuJOfnKY_-M5-fl1hW_25kSTNt33L57a5BgbGZ1sabWP3AD__-HYD2muR"
"klbfyYn_ghqjL7ihY2ECaZzZ0Utw",
encoded_token);
KeyBasedJwtVerifier jwt_verifier(jwks_server.url(),
/*jwks_verify_server_certificate*/ false,
/*jwks_ca_certificate*/ "");
ASSERT_OK(jwt_verifier.Init());
string subject;
ASSERT_OK(jwt_verifier.VerifyToken(encoded_token, &subject));
ASSERT_EQ("kudu", subject);
}
namespace {
const char kValidAccount[] = "new-phone";
const char kInvalidAccount[] = "who-is-this";
const char kMissingAccount[] = "no-one";
} // anonymous namespace
TEST(JwtUtilTest, VerifyOIDCDiscoveryEndpoint) {
MiniOidcOptions opts;
opts.account_ids = {
{ kValidAccount, /*is_valid*/true },
{ kInvalidAccount, /*is_valid*/false },
};
MiniOidc oidc(std::move(opts));
ASSERT_OK(oidc.Start());
const PerAccountKeyBasedJwtVerifier jwt_verifier(oidc.url(),
/*jwks_verify_server_certificate*/ false,
/*jwks_ca_certificate*/ "");
// Create and verify a token on the happy path.
const string kSubject = "kudu";
auto valid_user_token =
oidc.CreateJwt(kValidAccount, kSubject, /*is_valid*/true);
string subject;
ASSERT_OK(jwt_verifier.VerifyToken(valid_user_token, &subject));
ASSERT_EQ(kSubject, subject);
// Verify some expected failure scenarios.
const unordered_map<string, string> invalid_jwts {
{ oidc.CreateJwt(kInvalidAccount, kSubject, false), "invalid issuer with invalid "
"subject" },
{ oidc.CreateJwt(kInvalidAccount, kSubject, true), "invalid issuer with valid subject" },
{ oidc.CreateJwt(kValidAccount, kSubject, false), "valid issuer with invalid key id" },
{ oidc.CreateJwt(kMissingAccount, kSubject, true), "missing account" },
};
for (const auto& [jwt, msg] : invalid_jwts) {
string invalid_subject;
const Status s = jwt_verifier.VerifyToken(jwt, &invalid_subject);
EXPECT_FALSE(s.ok()) << Substitute("failed case $0: $1", msg, s.ToString());
}
}
TEST(JwtUtilTest, VerifyJWKSDiscoveryEndpointMultipleClients) {
MiniOidcOptions opts;
opts.account_ids = {
{ kValidAccount, /*is_valid*/true }
};
MiniOidc oidc(std::move(opts));
ASSERT_OK(oidc.Start());
PerAccountKeyBasedJwtVerifier jwt_verifier(oidc.url(),
/*jwks_verify_server_certificate*/ false,
/*jwks_ca_certificate*/ "");
{
const string kSubject = "kudu";
auto valid_user_token =
oidc.CreateJwt(kValidAccount, kSubject, /*is_valid*/true);
string subject;
ASSERT_OK(jwt_verifier.VerifyToken(valid_user_token, &subject));
ASSERT_EQ(kSubject, subject);
int constexpr n = 8;
std::vector<std::thread> threads;
threads.reserve(n);
CountDownLatch latch(n);
for (int i = 0; i < n; i++) {
threads.emplace_back([&](){
string subject;
CHECK_OK(jwt_verifier.VerifyToken(valid_user_token, &subject));
CHECK_EQ(kSubject, subject);
latch.CountDown();
});
}
latch.Wait();
SCOPED_CLEANUP({
for (auto& t : threads) {
t.join();
}
});
}
}
} // namespace kudu