IMPALA-10489: Implement JWT support

This patch added JWT support with following functionality:
 * Load and parse JWKS from pre-installed JSON file.
 * Read the JWT token from the HTTP Header.
 * Verify the JWT's signature with public key in JWKS.
 * Get the username out of the payload of JWT token.
 * Support following JSON Web Algorithms (JWA):
   HS256, HS384, HS512, RS256, RS384, RS512.

We use third party library jwt-cpp to verify JWT token. jwt-cpp is a
headers only C++ library. It was added to native-toolchain.
This patch modified bootstrap_toolchain.py to download jwt-cpp from
toolchain s3 bucket, and modified makefiles to add jwt-cpp/include
in the include path.

Added BE unit-tests for loading JWKS file and verifying JWT token.
Also added FE custom cluster test for JWT authentication.

Testing:
 - Passed core run.

Change-Id: I6b71fa854c9ddc8ca882878853395e1eb866143c
Reviewed-on: http://gerrit.cloudera.org:8080/17435
Reviewed-by: Joe McDonnell <joemcdonnell@cloudera.com>
Tested-by: Impala Public Jenkins <impala-public-jenkins@cloudera.com>
This commit is contained in:
wzhou-code
2021-05-06 22:34:25 -07:00
committed by Impala Public Jenkins
parent 59d32853ee
commit 025500ccb5
25 changed files with 2464 additions and 26 deletions

View File

@@ -102,6 +102,7 @@ set_dep_root(GFLAGS)
set_dep_root(GLOG)
set_dep_root(GPERFTOOLS)
set_dep_root(GTEST)
set_dep_root(JWT_CPP)
set_dep_root(LIBEV)
set_dep_root(LIBUNWIND)
set_dep_root(LLVM)
@@ -289,6 +290,11 @@ IMPALA_ADD_THIRDPARTY_LIB(zstd ${ZSTD_INCLUDE_DIR} ${ZSTD_STATIC_LIB} "")
find_package(Re2 REQUIRED)
IMPALA_ADD_THIRDPARTY_LIB(re2 ${RE2_INCLUDE_DIR} ${RE2_STATIC_LIB} "")
# find jwt-cpp headers
find_package(JwtCpp REQUIRED)
include_directories(${JWT_CPP_INCLUDE_DIR})
message(STATUS "jwt-cpp include dir: " ${JWT_CPP_INCLUDE_DIR})
# find rapidjson headers
find_package(RapidJson REQUIRED)
include_directories(${RAPIDJSON_INCLUDE_DIR})

View File

@@ -338,6 +338,7 @@ set(CLANG_INCLUDE_FLAGS
"-I${GLOG_INCLUDE_DIR}"
"-I${GFLAGS_INCLUDE_DIR}"
"-I${GTEST_INCLUDE_DIR}"
"-I${JWT_CPP_INCLUDE_DIR}"
"-I${RAPIDJSON_INCLUDE_DIR}"
"-I${AVRO_INCLUDE_DIR}"
"-I${ORC_INCLUDE_DIR}"

View File

@@ -83,7 +83,7 @@ class AuthProvider {
class SecureAuthProvider : public AuthProvider {
public:
SecureAuthProvider(bool is_internal)
: has_ldap_(false), has_saml_(false), is_internal_(is_internal) {}
: has_ldap_(false), has_saml_(false), has_jwt_(false), is_internal_(is_internal) {}
/// Performs initialization of external state.
/// If we're using ldap, set up appropriate certificate usage.
@@ -131,6 +131,8 @@ class SecureAuthProvider : public AuthProvider {
void InitSaml() { has_saml_ = true; }
void InitJwt() { has_jwt_ = true; }
/// Used for testing
const std::string& principal() const { return principal_; }
const std::string& service_name() const { return service_name_; }
@@ -144,6 +146,8 @@ class SecureAuthProvider : public AuthProvider {
bool has_saml_;
bool has_jwt_;
/// Hostname of this machine - if kerberos, derived from principal. If there
/// is no kerberos, but LDAP is used, then acquired via GetHostname().
std::string hostname_;

View File

@@ -57,6 +57,7 @@
#include "util/coding-util.h"
#include "util/debug-util.h"
#include "util/error-util.h"
#include "util/jwt-util.h"
#include "util/ldap-util.h"
#include "util/network-util.h"
#include "util/os-util.h"
@@ -137,6 +138,31 @@ DEFINE_bool_hidden(saml2_allow_without_tls_debug_only, false,
DECLARE_string(saml2_sp_callback_url);
// If set, Impala support for trusting an authentication based on JWT token in the HTTP
// header.
DEFINE_bool(jwt_token_auth, false,
"When true, read the JWT token out of the HTTP Header and extract user name from "
"the token payload.");
// The last segment of a JWT is the signature, which is used to verify that the token was
// signed by the sender and not altered in any way. By default, it's required to validate
// the signature of the JWT tokens. Otherwise it may expose security issue.
DEFINE_bool(jwt_validate_signature, true,
"When true, validate the signature of JWT token with pre-installed JWKS.");
// JWKS consists the public keys used by the signing party to the clients that need to
// validate signatures. It represents cryptographic keys in JSON data structure.
DEFINE_string(jwks_file_path, "",
"File path of the pre-installed JSON Web Key Set (JWKS) for JWT verification");
// This specifies the custom claim in the JWT that contains the "username" for the
// session.
DEFINE_string(jwt_custom_claim_username, "username", "Custom claim 'username'");
// If set, Impala allows JWT authentication on unsecure channel.
// JWT is only secure when used with TLS. But in some deployment scenarios, TLS is handled
// by proxy so that it does not show up as TLS to Impala.
DEFINE_bool_hidden(jwt_allow_without_tls, false,
"When this configuration is set to true, Impala allows JWT authentication on "
"unsecure channel. This should be only enabled for testing, or development for which "
"TLS is handled by proxy.");
namespace impala {
// Sasl callbacks. Why are these here? Well, Sasl isn't that bright, and
@@ -525,6 +551,41 @@ bool BasicAuth(ThriftServer::ConnectionContext* connection_context,
return false;
}
bool JWTTokenAuth(ThriftServer::ConnectionContext* connection_context,
const AuthenticationHash& hash, const string& token) {
JWTHelper::UniqueJWTDecodedToken decoded_token;
Status status = JWTHelper::Decode(token, decoded_token);
if (!status.ok()) {
LOG(ERROR) << "Error decoding JWT token received from: "
<< TNetworkAddressToString(connection_context->network_address)
<< " Error: " << status;
return false;
}
if (FLAGS_jwt_validate_signature) {
status = JWTHelper::GetInstance()->Verify(decoded_token.get());
if (!status.ok()) {
LOG(ERROR) << "Error verifying JWT token received from: "
<< TNetworkAddressToString(connection_context->network_address)
<< " Error: " << status;
return false;
}
}
DCHECK(!FLAGS_jwt_custom_claim_username.empty());
string username;
status = JWTHelper::GetCustomClaimUsername(
decoded_token.get(), FLAGS_jwt_custom_claim_username, username);
if (!status.ok()) {
LOG(ERROR) << "Error extracting username from JWT token received from: "
<< TNetworkAddressToString(connection_context->network_address)
<< " Error: " << status;
return false;
}
connection_context->username = username;
// TODO: cookies are not added, but are not needed right now
return true;
}
// Performs a step of SPNEGO auth for the HTTP transport and sets the username on
// 'connection_context' if auth is successful. 'header_token' is the value from an
// 'Authorization: Negotiate" header. Returns true if the step was successful and sets
@@ -1060,14 +1121,14 @@ Status SecureAuthProvider::Start() {
Status SecureAuthProvider::GetServerTransportFactory(
ThriftServer::TransportType underlying_transport_type, const std::string& server_name,
MetricGroup* metrics, std::shared_ptr<TTransportFactory>* factory) {
DCHECK(!principal_.empty() || has_ldap_ || has_saml_);
DCHECK(!principal_.empty() || has_ldap_ || has_saml_ || has_jwt_);
if (underlying_transport_type == ThriftServer::HTTP) {
bool has_kerberos = !principal_.empty();
bool use_cookies = FLAGS_max_cookie_lifetime_s > 0;
bool check_trusted_domain = !FLAGS_trusted_domain.empty();
factory->reset(new THttpServerTransportFactory(server_name, metrics, has_ldap_,
has_kerberos, use_cookies, check_trusted_domain, has_saml_));
has_kerberos, use_cookies, check_trusted_domain, has_saml_, has_jwt_));
return Status::OK();
}
@@ -1191,6 +1252,10 @@ void SecureAuthProvider::SetupConnectionContext(
callbacks.validate_saml2_bearer_fn =
std::bind(ValidateSaml2Bearer, connection_ptr.get(), hash_);
}
if (has_jwt_) {
callbacks.jwt_token_auth_fn =
std::bind(JWTTokenAuth, connection_ptr.get(), hash_, std::placeholders::_1);
}
http_input_transport->setCallbacks(callbacks);
http_output_transport->setCallbacks(callbacks);
socket = down_cast<TSocket*>(http_input_transport->getUnderlyingTransport().get());
@@ -1286,6 +1351,20 @@ Status AuthManager::Init() {
}
}
bool use_jwt = FLAGS_jwt_token_auth;
if (use_jwt) {
if (!IsExternalTlsConfigured()) {
if (!FLAGS_jwt_allow_without_tls) {
return Status("JWT authentication should be only used with TLS enabled.");
}
LOG(WARNING) << "JWT authentication is used without TLS.";
}
if (FLAGS_jwt_custom_claim_username.empty()) {
return Status(
"JWT authentication requires jwt_custom_claim_username to be specified.");
}
}
// Get all of the flag validation out of the way
if (FLAGS_enable_ldap_auth) {
RETURN_IF_ERROR(
@@ -1338,7 +1417,7 @@ Status AuthManager::Init() {
if (IsInternalKerberosEnabled()) {
// Initialize the auth provider first, in case validation of the principal fails.
SecureAuthProvider* sap = NULL;
SecureAuthProvider* sap = nullptr;
internal_auth_provider_.reset(sap = new SecureAuthProvider(true));
RETURN_IF_ERROR(sap->InitKerberos(kerberos_internal_principal));
LOG(INFO) << "Internal communication is authenticated with Kerberos";
@@ -1352,7 +1431,7 @@ Status AuthManager::Init() {
// principal or ldap tells us to use a SecureAuthProvider, and we fill in
// details from there.
if (FLAGS_enable_ldap_auth || external_kerberos_enabled) {
SecureAuthProvider* sap = NULL;
SecureAuthProvider* sap = nullptr;
external_auth_provider_.reset(sap = new SecureAuthProvider(false));
if (external_kerberos_enabled) {
RETURN_IF_ERROR(sap->InitKerberos(kerberos_external_principal));
@@ -1366,15 +1445,25 @@ Status AuthManager::Init() {
LOG(INFO) << "External communication can be also authenticated with SAML2 SSO";
sap->InitSaml();
}
if (use_jwt) {
LOG(INFO) << "External communication can be also authenticated with JWT";
sap->InitJwt();
}
} else {
external_auth_provider_.reset(new NoAuthProvider());
LOG(INFO) << "External communication is not authenticated for binary protocols";
if (use_saml) {
SecureAuthProvider* sap = NULL;
SecureAuthProvider* sap = nullptr;
external_http_auth_provider_.reset(sap = new SecureAuthProvider(false));
sap->InitSaml();
LOG(INFO) <<
"External communication is authenticated for hs2-http protocol with SAML2 SSO";
LOG(INFO) << "External communication is authenticated for hs2-http protocol with "
"SAML2 SSO";
} else if (use_jwt) {
SecureAuthProvider* sap = nullptr;
external_http_auth_provider_.reset(sap = new SecureAuthProvider(false));
sap->InitJwt();
LOG(INFO)
<< "External communication is authenticated for hs2-http protocol with JWT";
} else {
LOG(INFO) << "External communication is not authenticated for hs2-http protocol";
}

View File

@@ -87,6 +87,7 @@
#include "util/error-util.h"
#include "util/histogram-metric.h"
#include "util/impalad-metrics.h"
#include "util/jwt-util.h"
#include "util/metrics.h"
#include "util/network-util.h"
#include "util/openssl-util.h"
@@ -343,6 +344,11 @@ DEFINE_int32(admission_heartbeat_frequency_ms, 1000,
"admission service, if enabled. Heartbeats are used to ensure resources are properly "
"accounted for even if rpcs to the admission service occasionally fail.");
// Flags for JWT token based authentication.
DECLARE_bool(jwt_token_auth);
DECLARE_bool(jwt_validate_signature);
DECLARE_string(jwks_file_path);
namespace {
using namespace impala;
@@ -2869,6 +2875,17 @@ Status ImpalaServer::Start(int32_t beeswax_port, int32_t hs2_port,
LOG(INFO) << "Initialized executor Impala server on "
<< TNetworkAddressToString(exec_env_->configured_backend_address());
} else {
// Load JWKS from file if validation for signature of JWT token is enabled.
if (FLAGS_jwt_token_auth && FLAGS_jwt_validate_signature) {
if (!FLAGS_jwks_file_path.empty()) {
RETURN_IF_ERROR(JWTHelper::GetInstance()->Init(FLAGS_jwks_file_path));
} else {
LOG(ERROR) << "JWKS file is not specified when the validation of JWT signature "
<< " is enabled.";
return Status("JWKS file is not specified");
}
}
// Initialize the client servers.
shared_ptr<ImpalaServer> handler = shared_from_this();
if (beeswax_port > 0 || (TestInfo::is_test() && beeswax_port == 0)) {

View File

@@ -46,12 +46,13 @@ using strings::Substitute;
THttpServerTransportFactory::THttpServerTransportFactory(const std::string server_name,
impala::MetricGroup* metrics, bool has_ldap, bool has_kerberos, bool use_cookies,
bool check_trusted_domain, bool has_saml)
bool check_trusted_domain, bool has_saml, bool has_jwt)
: has_ldap_(has_ldap),
has_kerberos_(has_kerberos),
use_cookies_(use_cookies),
check_trusted_domain_(check_trusted_domain),
has_saml_(has_saml),
has_jwt_(has_jwt),
metrics_enabled_(metrics != nullptr) {
if (metrics_enabled_) {
if (has_ldap_) {
@@ -82,18 +83,25 @@ THttpServerTransportFactory::THttpServerTransportFactory(const std::string serve
http_metrics_.total_saml_auth_failure_ =
metrics->AddCounter(Substitute("$0.total-saml-auth-failure", server_name), 0);
}
if (has_jwt_) {
http_metrics_.total_jwt_token_auth_success_ = metrics->AddCounter(
Substitute("$0.total-jwt-token-auth-success", server_name), 0);
http_metrics_.total_jwt_token_auth_failure_ = metrics->AddCounter(
Substitute("$0.total-jwt-token-auth-failure", server_name), 0);
}
}
}
THttpServer::THttpServer(std::shared_ptr<TTransport> transport, bool has_ldap,
bool has_kerberos, bool has_saml, bool use_cookies, bool check_trusted_domain,
bool metrics_enabled, HttpMetrics* http_metrics)
bool has_jwt, bool metrics_enabled, HttpMetrics* http_metrics)
: THttpTransport(transport),
has_ldap_(has_ldap),
has_kerberos_(has_kerberos),
has_saml_(has_saml),
use_cookies_(use_cookies),
check_trusted_domain_(check_trusted_domain),
has_jwt_(has_jwt),
metrics_enabled_(metrics_enabled),
http_metrics_(http_metrics) {}
@@ -127,7 +135,7 @@ void THttpServer::parseHeader(char* header) {
contentLength_ = atoi(value);
} else if (strncmp(header, "X-Forwarded-For", sz) == 0) {
origin_ = value;
} else if ((has_ldap_ || has_kerberos_ || has_saml_)
} else if ((has_ldap_ || has_kerberos_ || has_saml_ || has_jwt_)
&& THRIFT_strncasecmp(header, "Authorization", sz) == 0) {
auth_value_ = string(value);
} else if (use_cookies_ && THRIFT_strncasecmp(header, "Cookie", sz) == 0) {
@@ -209,7 +217,7 @@ bool THttpServer::parseStatusLine(char* status) {
}
void THttpServer::headersDone() {
if (!has_ldap_ && !has_kerberos_ && !has_saml_) {
if (!has_ldap_ && !has_kerberos_ && !has_saml_ && !has_jwt_) {
// We don't need to authenticate.
resetAuthState();
return;
@@ -240,6 +248,25 @@ void THttpServer::headersDone() {
}
}
if (!authorized && has_jwt_ && !auth_value_.empty()
&& auth_value_.find('.') != string::npos) {
// Check Authorization header with the Bearer authentication scheme as:
// Authorization: Bearer <token>
// JWT contains at least one period ('.'). A well-formed JWT consists of three
// concatenated Base64url-encoded strings, separated by dots (.).
StripWhiteSpace(&auth_value_);
string jwt_token;
bool got_bearer_auth = TryStripPrefixString(auth_value_, "Bearer ", &jwt_token);
if (got_bearer_auth) {
if (callbacks_.jwt_token_auth_fn(jwt_token)) {
authorized = true;
if (metrics_enabled_) http_metrics_->total_jwt_token_auth_success_->Increment(1);
} else {
if (metrics_enabled_) http_metrics_->total_jwt_token_auth_failure_->Increment(1);
}
}
}
if (!authorized && has_saml_) {
bool fallback_to_other_auths = true;
if (saml_port_ != -1) {

View File

@@ -54,6 +54,9 @@ struct HttpMetrics {
impala::IntCounter* total_saml_auth_success_ = nullptr;
impala::IntCounter* total_saml_auth_failure_ = nullptr;
impala::IntCounter* total_jwt_token_auth_success_ = nullptr;
impala::IntCounter* total_jwt_token_auth_failure_ = nullptr;
};
/*
@@ -119,11 +122,17 @@ public:
// SAML2 SSO.
std::function<impala::TWrappedHttpRequest*()> init_wrapped_http_request_fn =
[&]() { return (impala::TWrappedHttpRequest*) NULL; };
// Function that takes the JWT token from the header, and returns true
// if verification for the token is successful.
std::function<bool(const std::string&)> jwt_token_auth_fn = [&](const std::string&) {
return false;
};
};
THttpServer(std::shared_ptr<TTransport> transport, bool has_ldap, bool has_kerberos,
bool has_saml, bool use_cookies, bool check_trusted_domain, bool metrics_enabled,
HttpMetrics* http_metrics);
bool has_saml, bool use_cookies, bool check_trusted_domain, bool has_jwt,
bool metrics_enabled, HttpMetrics* http_metrics);
virtual ~THttpServer();
@@ -147,9 +156,9 @@ protected:
void resetAuthState();
private:
// If either of the following is true, a '401 - Unauthorized' will be returned to the
// client on requests that do not contain a valid 'Authorization' of SAML SSO related
// header. If 'has_ldap_' is true, 'Basic' auth headers will be processed, and if
// 'has_kerberos_' is true 'Negotiate' auth headers will be processed.
// client on requests that do not contain a valid 'Authorization' of SAML SSO or JWT
// related header. If 'has_ldap_' is true, 'Basic' auth headers will be processed, and
// if 'has_kerberos_' is true 'Negotiate' auth headers will be processed.
bool has_ldap_ = false;
bool has_kerberos_ = false;
@@ -186,6 +195,9 @@ protected:
// trusted domain.
bool check_trusted_domain_ = false;
// If set, support for trusting an authentication based on JWT token.
bool has_jwt_ = false;
bool metrics_enabled_ = false;
HttpMetrics* http_metrics_ = nullptr;
@@ -203,13 +215,14 @@ public:
THttpServerTransportFactory(const std::string server_name, impala::MetricGroup* metrics,
bool has_ldap, bool has_kerberos, bool use_cookies, bool check_trusted_domain,
bool has_saml);
bool has_saml, bool has_jwt);
virtual ~THttpServerTransportFactory() {}
virtual std::shared_ptr<TTransport> getTransport(std::shared_ptr<TTransport> trans) {
return std::shared_ptr<TTransport>(new THttpServer(trans, has_ldap_, has_kerberos_,
has_saml_, use_cookies_, check_trusted_domain_, metrics_enabled_, &http_metrics_));
return std::shared_ptr<TTransport>(
new THttpServer(trans, has_ldap_, has_kerberos_, has_saml_, use_cookies_,
check_trusted_domain_, has_jwt_, metrics_enabled_, &http_metrics_));
}
private:
@@ -218,6 +231,7 @@ public:
bool use_cookies_ = false;
bool check_trusted_domain_ = false;
bool has_saml_ = false;
bool has_jwt_ = false;
// Metrics for every transport produced by this factory.
bool metrics_enabled_ = false;

View File

@@ -63,6 +63,7 @@ set(UTIL_SRCS
impala-bloom-filter-buffer-allocator.cc
jni-util.cc
json-util.cc
jwt-util.cc
ldap-util.cc
ldap-search-bind.cc
ldap-simple-bind.cc
@@ -129,6 +130,7 @@ add_library(UtilTests STATIC
fixed-size-hash-table-test.cc
hdfs-util-test.cc
hdr-histogram-test.cc
jwt-util-test.cc
logging-support-test.cc
metrics-test.cc
min-max-filter-test.cc
@@ -198,6 +200,7 @@ ADD_UNIFIED_BE_LSAN_TEST(hdr-histogram-test HdrHistogramTest.*)
# internal-queue-test has a non-standard main(), so it needs a small amount of thought
# to use a unified executable
ADD_BE_LSAN_TEST(internal-queue-test)
ADD_UNIFIED_BE_LSAN_TEST(jwt-util-test "JwtUtilTest.*")
ADD_UNIFIED_BE_LSAN_TEST(logging-support-test "LoggingSupport.*")
ADD_UNIFIED_BE_LSAN_TEST(metrics-test "MetricsTest.*")
ADD_UNIFIED_BE_LSAN_TEST(min-max-filter-test "MinMaxFilterTest.*")

View File

@@ -0,0 +1,224 @@
// 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.
#ifndef IMPALA_JWT_UTIL_INTERNAL_H
#define IMPALA_JWT_UTIL_INTERNAL_H
#include <string>
#include <unordered_map>
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wreserved-id-macro"
#pragma clang diagnostic ignored "-Wunused-private-field"
// picojson/picojson.h which is included by jwt-cpp/jwt.h defines __STDC_FORMAT_MACROS
// without checking if it's already defined so un-define the micro here to avoid
// re-definition error. Also need to hide warning "macro name is a reserved identifier".
#ifdef __STDC_FORMAT_MACROS
#undef __STDC_FORMAT_MACROS
#endif
#include <jwt-cpp/jwt.h>
#pragma clang diagnostic pop
#include "common/logging.h"
#include "common/status.h"
namespace impala {
using DecodedJWT = jwt::decoded_jwt<jwt::picojson_traits>;
using JWTVerifier = jwt::verifier<jwt::default_clock, jwt::picojson_traits>;
/// Key-Value map for parsing Json keys.
typedef std::unordered_map<std::string, std::string> JsonKVMap;
class JsonWebKeySet;
/// JWTPublicKey:
/// This class represent cryptographic public key for JSON Web Token (JWT) verification.
class JWTPublicKey {
public:
JWTPublicKey(std::string algorithm, std::string pub_key)
: verifier_(jwt::verify()), algorithm_(algorithm), public_key_(pub_key) {}
/// Verify the given decoded token.
Status Verify(const DecodedJWT& decoded_jwt, const std::string& algorithm) const;
const std::string& get_algorithm() const { return algorithm_; }
const std::string& get_key() const { return public_key_; }
protected:
/// JWT Verifier.
JWTVerifier verifier_;
private:
/// Signing Algorithm:
/// Currently support following JSON Web Algorithms (JWA):
/// HS256, HS384, HS512, RS256, RS384, and RS512.
const std::string algorithm_;
/// Public key value:
/// For EC and RSA families of algorithms, it's the public key converted in PEM-encoded
/// format since jwt-cpp APIs only accept EC/RSA public keys in PEM-encoded format.
/// For HMAC-SHA2, it's Octet Sequence key representing secret key.
const std::string public_key_;
};
/// JWT Public Key for HS256.
/// HS256: HMAC using SHA-256.
class HS256JWTPublicKey : public JWTPublicKey {
public:
/// Throw JWT exception if failed to initialize the verifier.
HS256JWTPublicKey(std::string algorithm, std::string pub_key)
: JWTPublicKey(algorithm, pub_key) {
verifier_.allow_algorithm(jwt::algorithm::hs256(pub_key));
}
};
/// JWT Public Key for HS384.
/// HS384: HMAC using SHA-384.
class HS384JWTPublicKey : public JWTPublicKey {
public:
/// Throw exception if failed to initialize the JWT verifier.
HS384JWTPublicKey(std::string algorithm, std::string pub_key)
: JWTPublicKey(algorithm, pub_key) {
verifier_.allow_algorithm(jwt::algorithm::hs384(pub_key));
}
};
/// JWT Public Key for HS512.
/// HS512: HMAC using SHA-512.
class HS512JWTPublicKey : public JWTPublicKey {
public:
/// Throw JWT exception if failed to initialize the verifier.
HS512JWTPublicKey(std::string algorithm, std::string pub_key)
: JWTPublicKey(algorithm, pub_key) {
verifier_.allow_algorithm(jwt::algorithm::hs512(pub_key));
}
};
/// JWT Public Key for RS256.
/// RS256: RSASSA-PKCS1-v1_5 using SHA-256.
class RS256JWTPublicKey : public JWTPublicKey {
public:
/// Throw JWT exception if failed to initialize the verifier.
RS256JWTPublicKey(std::string algorithm, std::string pub_key)
: JWTPublicKey(algorithm, pub_key) {
verifier_.allow_algorithm(jwt::algorithm::rs256(pub_key, "", "", ""));
}
};
/// JWT Public Key for RS384.
/// RS384: RSASSA-PKCS1-v1_5 using SHA-384.
class RS384JWTPublicKey : public JWTPublicKey {
public:
/// Throw exception if failed to initialize the JWT verifier.
RS384JWTPublicKey(std::string algorithm, std::string pub_key)
: JWTPublicKey(algorithm, pub_key) {
verifier_.allow_algorithm(jwt::algorithm::rs384(pub_key, "", "", ""));
}
};
/// JWT Public Key for RS512.
/// RS512: RSASSA-PKCS1-v1_5 using SHA-512.
class RS512JWTPublicKey : public JWTPublicKey {
public:
/// Throw JWT exception if failed to initialize the verifier.
RS512JWTPublicKey(std::string algorithm, std::string pub_key)
: JWTPublicKey(algorithm, pub_key) {
verifier_.allow_algorithm(jwt::algorithm::rs512(pub_key, "", "", ""));
}
};
/// Construct a JWKPublicKey of HS from the JWK.
class HSJWTPublicKeyBuilder {
public:
static Status CreateJWKPublicKey(JsonKVMap& kv_map, JWTPublicKey** pub_key_out);
};
/// Construct a JWKPublicKey of RSA from the JWK.
class RSAJWTPublicKeyBuilder {
public:
static Status CreateJWKPublicKey(JsonKVMap& kv_map, JWTPublicKey** pub_key_out);
private:
/// Convert public key of RSA from JWK format to PEM encoded format by using OpenSSL
/// APIs.
static bool ConvertJwkToPem(
const std::string& base64_n, const std::string& base64_e, std::string& pub_key);
};
/// JSON Web Key Set (JWKS) conveys the public keys used by the signing party to the
/// clients that need to validate signatures. It represents a cryptographic key set in
/// JSON data structure.
/// This class works as JWT provider, which load the JWKS from file, store keys in an
/// internal maps for each family of algorithms, and provides API to retrieve key by
/// key-id.
/// Init() should be called during the initialization of the daemon. There is no
/// more modification for the instance after Init() return. The class is thread safe.
class JsonWebKeySet {
public:
explicit JsonWebKeySet() {}
/// Map from a key ID (kid) to a JWTPublicKey.
typedef std::unordered_map<std::string, std::unique_ptr<JWTPublicKey>> JWTPublicKeyMap;
/// Load JWKS stored in a JSON file. Returns an error if problems were encountered
/// while parsing/constructing the Json Web keys. If no keys were given in the file,
/// the internal maps will be empty.
Status Init(const std::string& jwks_file_path);
/// Look up the key ID in the internal key maps and returns the key if the lookup was
/// successful, otherwise return nullptr.
const JWTPublicKey* LookupRSAPublicKey(const std::string& kid) const;
const JWTPublicKey* LookupHSKey(const std::string& kid) const;
/// Return number of keys for each family of algorithms.
int GetHSKeyNum() const { return hs_key_map_.size(); }
/// Return number of keys for RSA.
int GetRSAPublicKeyNum() const { return rsa_pub_key_map_.size(); }
/// Return all keys for HS.
const JWTPublicKeyMap* GetAllHSKeys() const { return &hs_key_map_; }
/// Return all keys for RSA.
const JWTPublicKeyMap* GetAllRSAPublicKeys() const { return &rsa_pub_key_map_; }
/// Return TRUE if there is no key.
bool IsEmpty() const { return hs_key_map_.empty() && rsa_pub_key_map_.empty(); }
private:
friend class JWKSetParser;
/// Following two functions are called inside Init().
/// Add a RSA public key.
void AddRSAPublicKey(std::string key_id, JWTPublicKey* jwk_pub_key);
/// Add a HS key.
void AddHSKey(std::string key_id, JWTPublicKey* jwk_pub_key);
/// Note: According to section 4.5 of RFC 7517 (JSON Web Key), different keys might use
/// the same "kid" value is if they have different "kty" (key type) values but are
/// considered to be equivalent alternatives by the application using them. So keys
/// for each "kty" are saved in different maps.
/// Octet Sequence keys for HS256 (HMAC using SHA-256), HS384 and HS512.
/// kty (key type): oct.
JWTPublicKeyMap hs_key_map_;
/// Public keys for RSA family of algorithms: RS256, RS384, RS512.
/// kty (key type): RSA.
JWTPublicKeyMap rsa_pub_key_map_;
};
} // namespace impala
#endif

View File

@@ -0,0 +1,702 @@
// 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.
#include <cstdio> // file stuff
#include <gutil/strings/substitute.h>
#include "jwt-util-internal.h"
#include "jwt-util.h"
#include "testutil/gtest-util.h"
#include "common/names.h"
namespace impala {
using std::string;
std::string rsa_priv_key_pem = R"(-----BEGIN PRIVATE KEY-----
MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQC4ZtdaIrd1BPIJ
tfnF0TjIK5inQAXZ3XlCrUlJdP+XHwIRxdv1FsN12XyMYO/6ymLmo9ryoQeIrsXB
XYqlET3zfAY+diwCb0HEsVvhisthwMU4gZQu6TYW2s9LnXZB5rVtcBK69hcSlA2k
ZudMZWxZcj0L7KMfO2rIvaHw/qaVOE9j0T257Z8Kp2CLF9MUgX0ObhIsdumFRLaL
DvDUmBPr2zuh/34j2XmWwn1yjN/WvGtdfhXW79Ki1S40HcWnygHgLV8sESFKUxxQ
mKvPUTwDOIwLFL5WtE8Mz7N++kgmDcmWMCHc8kcOIu73Ta/3D4imW7VbKgHZo9+K
3ESFE3RjAgMBAAECggEBAJTEIyjMqUT24G2FKiS1TiHvShBkTlQdoR5xvpZMlYbN
tVWxUmrAGqCQ/TIjYnfpnzCDMLhdwT48Ab6mQJw69MfiXwc1PvwX1e9hRscGul36
ryGPKIVQEBsQG/zc4/L2tZe8ut+qeaK7XuYrPp8bk/X1e9qK5m7j+JpKosNSLgJj
NIbYsBkG2Mlq671irKYj2hVZeaBQmWmZxK4fw0Istz2WfN5nUKUeJhTwpR+JLUg4
ELYYoB7EO0Cej9UBG30hbgu4RyXA+VbptJ+H042K5QJROUbtnLWuuWosZ5ATldwO
u03dIXL0SH0ao5NcWBzxU4F2sBXZRGP2x/jiSLHcqoECgYEA4qD7mXQpu1b8XO8U
6abpKloJCatSAHzjgdR2eRDRx5PMvloipfwqA77pnbjTUFajqWQgOXsDTCjcdQui
wf5XAaWu+TeAVTytLQbSiTsBhrnoqVrr3RoyDQmdnwHT8aCMouOgcC5thP9vQ8Us
rVdjvRRbnJpg3BeSNimH+u9AHgsCgYEA0EzcbOltCWPHRAY7B3Ge/AKBjBQr86Kv
TdpTlxePBDVIlH+BM6oct2gaSZZoHbqPjbq5v7yf0fKVcXE4bSVgqfDJ/sZQu9Lp
PTeV7wkk0OsAMKk7QukEpPno5q6tOTNnFecpUhVLLlqbfqkB2baYYwLJR3IRzboJ
FQbLY93E8gkCgYB+zlC5VlQbbNqcLXJoImqItgQkkuW5PCgYdwcrSov2ve5r/Acz
FNt1aRdSlx4176R3nXyibQA1Vw+ztiUFowiP9WLoM3PtPZwwe4bGHmwGNHPIfwVG
m+exf9XgKKespYbLhc45tuC08DATnXoYK7O1EnUINSFJRS8cezSI5eHcbQKBgQDC
PgqHXZ2aVftqCc1eAaxaIRQhRmY+CgUjumaczRFGwVFveP9I6Gdi+Kca3DE3F9Pq
PKgejo0SwP5vDT+rOGHN14bmGJUMsX9i4MTmZUZ5s8s3lXh3ysfT+GAhTd6nKrIE
kM3Nh6HWFhROptfc6BNusRh1kX/cspDplK5x8EpJ0QKBgQDWFg6S2je0KtbV5PYe
RultUEe2C0jYMDQx+JYxbPmtcopvZQrFEur3WKVuLy5UAy7EBvwMnZwIG7OOohJb
vkSpADK6VPn9lbqq7O8cTedEHttm6otmLt8ZyEl3hZMaL3hbuRj6ysjmoFKx6CrX
rK0/Ikt5ybqUzKCMJZg2VKGTxg==
-----END PRIVATE KEY-----)";
std::string rsa_pub_key_pem = R"(-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuGbXWiK3dQTyCbX5xdE4
yCuYp0AF2d15Qq1JSXT/lx8CEcXb9RbDddl8jGDv+spi5qPa8qEHiK7FwV2KpRE9
83wGPnYsAm9BxLFb4YrLYcDFOIGULuk2FtrPS512Qea1bXASuvYXEpQNpGbnTGVs
WXI9C+yjHztqyL2h8P6mlThPY9E9ue2fCqdgixfTFIF9Dm4SLHbphUS2iw7w1JgT
69s7of9+I9l5lsJ9cozf1rxrXX4V1u/SotUuNB3Fp8oB4C1fLBEhSlMcUJirz1E8
AziMCxS+VrRPDM+zfvpIJg3JljAh3PJHDiLu902v9w+Iplu1WyoB2aPfitxEhRN0
YwIDAQAB
-----END PUBLIC KEY-----)";
std::string rsa_pub_key_jwk_n =
"uGbXWiK3dQTyCbX5xdE4yCuYp0AF2d15Qq1JSXT_lx8CEcXb9RbDddl8jGDv-sp"
"i5qPa8qEHiK7FwV2KpRE983wGPnYsAm9BxLFb4YrLYcDFOIGULuk2FtrPS512Qe"
"a1bXASuvYXEpQNpGbnTGVsWXI9C-yjHztqyL2h8P6mlThPY9E9ue2fCqdgixfTF"
"IF9Dm4SLHbphUS2iw7w1JgT69s7of9-I9l5lsJ9cozf1rxrXX4V1u_SotUuNB3F"
"p8oB4C1fLBEhSlMcUJirz1E8AziMCxS-VrRPDM-zfvpIJg3JljAh3PJHDiLu902"
"v9w-Iplu1WyoB2aPfitxEhRN0Yw";
std::string rsa_pub_key_jwk_e = "AQAB";
std::string rsa_invalid_pub_key_jwk_n =
"xzYuc22QSst_dS7geYYK5l5kLxU0tayNdixkEQ17ix-CUcUbKIsnyftZxaCYT46"
"rQtXgCaYRdJcbB3hmyrOavkhTpX79xJZnQmfuamMbZBqitvscxW9zRR9tBUL6vd"
"i_0rpoUwPMEh8-Bw7CgYR0FK0DhWYBNDfe9HKcyZEv3max8Cdq18htxjEsdYO0i"
"wzhtKRXomBWTdhD5ykd_fACVTr4-KEY-IeLvubHVmLUhbE5NgWXxrRpGasDqzKh"
"CTmsa2Ysf712rl57SlH0Wz_Mr3F7aM9YpErzeYLrl0GhQr9BVJxOvXcVd4kmY-X"
"kiCcrkyS1cnghnllh-LCwQu1sYw";
std::string rsa512_priv_key_pem = R"(-----BEGIN RSA PRIVATE KEY-----
MIICWwIBAAKBgQDdlatRjRjogo3WojgGHFHYLugdUWAY9iR3fy4arWNA1KoS8kVw
33cJibXr8bvwUAUparCwlvdbH6dvEOfou0/gCFQsHUfQrSDv+MuSUMAe8jzKE4qW
+jK+xQU9a03GUnKHkkle+Q0pX/g6jXZ7r1/xAK5Do2kQ+X5xK9cipRgEKwIDAQAB
AoGAD+onAtVye4ic7VR7V50DF9bOnwRwNXrARcDhq9LWNRrRGElESYYTQ6EbatXS
3MCyjjX2eMhu/aF5YhXBwkppwxg+EOmXeh+MzL7Zh284OuPbkglAaGhV9bb6/5Cp
uGb1esyPbYW+Ty2PC0GSZfIXkXs76jXAu9TOBvD0ybc2YlkCQQDywg2R/7t3Q2OE
2+yo382CLJdrlSLVROWKwb4tb2PjhY4XAwV8d1vy0RenxTB+K5Mu57uVSTHtrMK0
GAtFr833AkEA6avx20OHo61Yela/4k5kQDtjEf1N0LfI+BcWZtxsS3jDM3i1Hp0K
Su5rsCPb8acJo5RO26gGVrfAsDcIXKC+bQJAZZ2XIpsitLyPpuiMOvBbzPavd4gY
6Z8KWrfYzJoI/Q9FuBo6rKwl4BFoToD7WIUS+hpkagwWiz+6zLoX1dbOZwJACmH5
fSSjAkLRi54PKJ8TFUeOP15h9sQzydI8zJU+upvDEKZsZc/UhT/SySDOxQ4G/523
Y0sz/OZtSWcol/UMgQJALesy++GdvoIDLfJX5GBQpuFgFenRiRDabxrE9MNUZ2aP
FaFp+DyAe+b4nDwuJaW2LURbr8AEZga7oQj0uYxcYw==
-----END RSA PRIVATE KEY-----)";
std::string rsa512_pub_key_pem = R"(-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDdlatRjRjogo3WojgGHFHYLugd
UWAY9iR3fy4arWNA1KoS8kVw33cJibXr8bvwUAUparCwlvdbH6dvEOfou0/gCFQs
HUfQrSDv+MuSUMAe8jzKE4qW+jK+xQU9a03GUnKHkkle+Q0pX/g6jXZ7r1/xAK5D
o2kQ+X5xK9cipRgEKwIDAQAB
-----END PUBLIC KEY-----)";
std::string rsa512_pub_key_jwk_n =
"3ZWrUY0Y6IKN1qI4BhxR2C7oHVFgGPYkd38uGq1jQNSqEvJFcN93CYm16_G78FA"
"FKWqwsJb3Wx-nbxDn6LtP4AhULB1H0K0g7_jLklDAHvI8yhOKlvoyvsUFPWtNxl"
"Jyh5JJXvkNKV_4Oo12e69f8QCuQ6NpEPl-cSvXIqUYBCs";
std::string rsa512_pub_key_jwk_e = "AQAB";
std::string rsa512_invalid_pub_key_jwk_n =
"xzYuc22QSst_dS7geYYK5l5kLxU0tayNdixkEQ17ix-CUcUbKIsnyftZxaCYT46"
"rQtXgCaYRdJcbB3hmyrOavkhTpX79xJZnQmfuamMbZBqitvscxW9zRR9tBUL6vd"
"i_0rpoUwPMEh8-Bw7CgYR0FK0DhWYBNDfe9HKcyZEv3max8Cdq18htxjEsdYO0i"
"wzhtKRXomBWTdhD5ykd_fACVTr4-KEY-IeLvubHVmLUhbE5NgWXxrRpGasDqzKh"
"CTmsa2Ysf712rl57SlH0Wz_Mr3F7aM9YpErzeYLrl0GhQr9BVJxOvXcVd4kmY-X"
"kiCcrkyS1cnghnllh-LCwQu1sYw";
std::string kid_1 = "public:c424b67b-fe28-45d7-b015-f79da50b5b21";
std::string kid_2 = "public:9b9d0b47-b9ed-4ba6-9180-52fc5b161a3a";
std::string jwks_hs_file_format = R"(
{
"keys": [
{ "kty": "oct", "kid": "$0", "alg": "$1", "k": "$2" }
]
})";
std::string jwks_rsa_file_format = R"(
{
"keys": [
{ "kty": "RSA", "kid": "$0", "alg": "$1", "n": "$2", "e": "$3" },
{ "kty": "RSA", "kid": "$4", "alg": "$5", "n": "$6", "e": "$7" }
]
})";
/// 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.
TempTestDataFile(const std::string& contents);
~TempTestDataFile() { Delete(); }
/// Returns the absolute path to the file.
const std::string& Filename() const { return name_; }
private:
std::string name_;
bool deleted_;
// Delete this temporary file
void Delete();
};
TempTestDataFile::TempTestDataFile(const std::string& contents)
: name_("/tmp/jwks_XXXXXX"), deleted_(false) {
int fd = mkstemp(&name_[0]);
if (fd == -1) {
std::cout << "Error creating temp file; " << strerror(errno) << std::endl;
abort();
}
if (close(fd) != 0) {
std::cout << "Error closing temp file; " << strerror(errno) << std::endl;
abort();
}
FILE* handle = fopen(name_.c_str(), "w");
if (handle == nullptr) {
std::cout << "Error creating temp file; " << strerror(errno) << std::endl;
abort();
}
int status = fputs(contents.c_str(), handle);
if (status < 0) {
std::cout << "Error writing to temp file; " << strerror(errno) << std::endl;
abort();
}
status = fclose(handle);
if (status != 0) {
std::cout << "Error closing temp file; " << strerror(errno) << std::endl;
abort();
}
}
void TempTestDataFile::Delete() {
if (deleted_) return;
deleted_ = true;
if (remove(name_.c_str()) != 0) {
std::cout << "Error deleting temp file; " << strerror(errno) << std::endl;
abort();
}
}
TEST(JwtUtilTest, LoadJwksFile) {
// Load JWKS from file.
TempTestDataFile jwks_file(Substitute(jwks_rsa_file_format, kid_1, "RS256",
rsa_pub_key_jwk_n, rsa_pub_key_jwk_e, kid_2, "RS256", rsa_invalid_pub_key_jwk_n,
rsa_pub_key_jwk_e));
JWTHelper jwt_helper;
Status status = jwt_helper.Init(jwks_file.Filename());
EXPECT_OK(status);
const JsonWebKeySet* jwks = jwt_helper.GetJWKS();
ASSERT_FALSE(jwks->IsEmpty());
ASSERT_EQ(2, jwks->GetRSAPublicKeyNum());
const JWTPublicKey* key1 = jwks->LookupRSAPublicKey(kid_1);
ASSERT_TRUE(key1 != nullptr);
ASSERT_EQ("rs256", key1->get_algorithm());
ASSERT_EQ(rsa_pub_key_pem, 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_TRUE(status.msg().msg().find("parsing key #0") != std::string::npos)
<< " Actual error: " << status.msg().msg();
ASSERT_TRUE(status.GetDetail().find("'kid' property is required") != std::string::npos)
<< "actual error: " << status.GetDetail();
ASSERT_TRUE(jwt_helper.GetJWKS()->IsEmpty());
// 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_TRUE(status.GetDetail().find("Missing a comma or ']' after an array element")
!= std::string::npos)
<< " Actual error: " << status.GetDetail();
// JWKS with empty key id.
jwks_file.reset(new TempTestDataFile(
Substitute(jwks_rsa_file_format, "", "RS256", rsa_pub_key_jwk_n, rsa_pub_key_jwk_e,
"", "RS256", rsa_invalid_pub_key_jwk_n, rsa_pub_key_jwk_e)));
status = jwt_helper.Init(jwks_file->Filename());
ASSERT_FALSE(status.ok());
ASSERT_TRUE(status.msg().msg().find("parsing key #0") != std::string::npos)
<< " Actual error: " << status.msg().msg();
ASSERT_TRUE(status.GetDetail().find("'kid' property must be a non-empty string")
!= std::string::npos)
<< " Actual error: " << status.GetDetail();
// JWKS with empty key value.
jwks_file.reset(new TempTestDataFile(
Substitute(jwks_rsa_file_format, kid_1, "RS256", "", "", kid_2, "RS256", "", "")));
status = jwt_helper.Init(jwks_file->Filename());
ASSERT_FALSE(status.ok());
ASSERT_TRUE(status.msg().msg().find("parsing key #0") != std::string::npos)
<< " Actual error: " << status.msg().msg();
ASSERT_TRUE(status.GetDetail().find("'n' and 'e' properties must be a non-empty string")
!= std::string::npos)
<< " Actual error: " << status.GetDetail();
}
TEST(JwtUtilTest, VerifyJwtHS256) {
// Cryptographic algorithm: HS256.
// SharedSecret (Generated for MAC key (Base64 encoded)).
string shared_secret = "Yx57JSBzhGFDgDj19CabRpH/+kiaKqI6UZI6lDunQKw=";
TempTestDataFile jwks_file(
Substitute(jwks_hs_file_format, kid_1, "HS256", shared_secret));
JWTHelper jwt_helper;
Status status = jwt_helper.Init(jwks_file.Filename());
EXPECT_OK(status);
const JsonWebKeySet* jwks = jwt_helper.GetJWKS();
EXPECT_OK(status);
ASSERT_EQ(1, jwks->GetHSKeyNum());
const JWTPublicKey* key1 = jwks->LookupHSKey(kid_1);
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(kid_1)
.set_payload_claim("username", picojson::value("impala"))
.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("impala", username);
}
TEST(JwtUtilTest, VerifyJwtHS384) {
// Cryptographic algorithm: HS384.
// SharedSecret (Generated for MAC key (Base64 encoded)).
string shared_secret =
"TlqmKRc2PNQJXTC3Go7eAadwPxA7x9byyXCi5I8tSvxrE77tYbuF5pfZAyswrkou";
TempTestDataFile jwks_file(
Substitute(jwks_hs_file_format, kid_1, "HS384", shared_secret));
JWTHelper jwt_helper;
Status status = jwt_helper.Init(jwks_file.Filename());
EXPECT_OK(status);
const JsonWebKeySet* jwks = jwt_helper.GetJWKS();
EXPECT_OK(status);
ASSERT_EQ(1, jwks->GetHSKeyNum());
const JWTPublicKey* key1 = jwks->LookupHSKey(kid_1);
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(kid_1)
.set_payload_claim("username", picojson::value("impala"))
.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("impala", username);
}
TEST(JwtUtilTest, VerifyJwtHS512) {
// Cryptographic algorithm: HS512.
// SharedSecret (Generated for MAC key (Base64 encoded)).
string shared_secret = "ywc6DN7+iRw1E5HOqzvrsYodykSLFutT28KN3bJnLZcZpPCNjn0b6gbMfXPcxeY"
"VyuWWGDxh6gCDwPMejbuEEg==";
TempTestDataFile jwks_file(
Substitute(jwks_hs_file_format, kid_1, "HS512", shared_secret));
JWTHelper jwt_helper;
Status status = jwt_helper.Init(jwks_file.Filename());
EXPECT_OK(status);
const JsonWebKeySet* jwks = jwt_helper.GetJWKS();
EXPECT_OK(status);
ASSERT_EQ(1, jwks->GetHSKeyNum());
const JWTPublicKey* key1 = jwks->LookupHSKey(kid_1);
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(kid_1)
.set_payload_claim("username", picojson::value("impala"))
.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("impala", username);
}
TEST(JwtUtilTest, VerifyJwtRS256) {
// Cryptographic algorithm: RS256.
TempTestDataFile jwks_file(Substitute(jwks_rsa_file_format, kid_1, "RS256",
rsa_pub_key_jwk_n, rsa_pub_key_jwk_e, kid_2, "RS256", rsa_invalid_pub_key_jwk_n,
rsa_pub_key_jwk_e));
JWTHelper jwt_helper;
Status status = jwt_helper.Init(jwks_file.Filename());
EXPECT_OK(status);
const JsonWebKeySet* jwks = jwt_helper.GetJWKS();
ASSERT_EQ(2, jwks->GetRSAPublicKeyNum());
const JWTPublicKey* key1 = jwks->LookupRSAPublicKey(kid_1);
ASSERT_TRUE(key1 != nullptr);
ASSERT_EQ(rsa_pub_key_pem, 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(kid_1)
.set_payload_claim("username", picojson::value("impala"))
.sign(jwt::algorithm::rs256(rsa_pub_key_pem, rsa_priv_key_pem, "", ""));
ASSERT_EQ(
"eyJhbGciOiJSUzI1NiIsImtpZCI6InB1YmxpYzpjNDI0YjY3Yi1mZTI4LTQ1ZDctYjAxNS1mNzlkYTUwYj"
"ViMjEiLCJ0eXAiOiJKV1MifQ.eyJpc3MiOiJhdXRoMCIsInVzZXJuYW1lIjoiaW1wYWxhIn0.OW5H2SClL"
"lsotsCarTHYEbqlbRh43LFwOyo9WubpNTwE7hTuJDsnFoVrvHiWI02W69TZNat7DYcC86A_ogLMfNXagHj"
"lMFJaRnvG5Ekag8NRuZNJmHVqfX-qr6x7_8mpOdU554kc200pqbpYLhhuK4Qf7oT7y9mOrtNrUKGDCZ0Q2"
"y_mizlbY6SMg4RWqSz0RQwJbRgXIWSgcbZd0GbD_MQQ8x7WRE4nluU-5Fl4N2Wo8T9fNTuxALPiuVeIczO"
"25b5n4fryfKasSgaZfmk0CoOJzqbtmQxqiK9QNSJAiH2kaqMwLNgAdgn8fbd-lB1RAEGeyPH8Px8ipqcKs"
"Pk0bg",
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(rsa_pub_key_pem, "", "", ""))
.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("impala", username);
}
TEST(JwtUtilTest, VerifyJwtRS384) {
// Cryptographic algorithm: RS384.
TempTestDataFile jwks_file(Substitute(jwks_rsa_file_format, kid_1, "RS384",
rsa_pub_key_jwk_n, rsa_pub_key_jwk_e, kid_2, "RS384", rsa_invalid_pub_key_jwk_n,
rsa_pub_key_jwk_e));
JWTHelper jwt_helper;
Status status = jwt_helper.Init(jwks_file.Filename());
EXPECT_OK(status);
const JsonWebKeySet* jwks = jwt_helper.GetJWKS();
ASSERT_EQ(2, jwks->GetRSAPublicKeyNum());
const JWTPublicKey* key1 = jwks->LookupRSAPublicKey(kid_1);
ASSERT_TRUE(key1 != nullptr);
ASSERT_EQ(rsa_pub_key_pem, 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(kid_1)
.set_payload_claim("username", picojson::value("impala"))
.sign(jwt::algorithm::rs384(rsa_pub_key_pem, rsa_priv_key_pem, "", ""));
// 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("impala", username);
}
TEST(JwtUtilTest, VerifyJwtRS512) {
// Cryptographic algorithm: RS512.
TempTestDataFile jwks_file(Substitute(jwks_rsa_file_format, kid_1, "RS512",
rsa512_pub_key_jwk_n, rsa512_pub_key_jwk_e, kid_2, "RS512",
rsa512_invalid_pub_key_jwk_n, rsa512_pub_key_jwk_e));
JWTHelper jwt_helper;
Status status = jwt_helper.Init(jwks_file.Filename());
EXPECT_OK(status);
const JsonWebKeySet* jwks = jwt_helper.GetJWKS();
ASSERT_EQ(2, jwks->GetRSAPublicKeyNum());
const JWTPublicKey* key1 = jwks->LookupRSAPublicKey(kid_1);
ASSERT_TRUE(key1 != nullptr);
ASSERT_EQ(rsa512_pub_key_pem, 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(kid_1)
.set_payload_claim("username", picojson::value("impala"))
.sign(jwt::algorithm::rs512(rsa512_pub_key_pem, rsa512_priv_key_pem, "", ""));
// 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("impala", 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("impala"))
.sign(jwt::algorithm::rs256(rsa_pub_key_pem, rsa_priv_key_pem, "", ""));
// 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("impala", username);
}
TEST(JwtUtilTest, VerifyJwtFailMismatchingAlgorithms) {
// JWT algorithm is not matching with algorithm in JWK.
TempTestDataFile jwks_file(Substitute(jwks_rsa_file_format, kid_1, "RS256",
rsa_pub_key_jwk_n, rsa_pub_key_jwk_e, kid_2, "RS256", rsa_invalid_pub_key_jwk_n,
rsa_pub_key_jwk_e));
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(kid_1)
.sign(jwt::algorithm::rs256(rsa_pub_key_pem, rsa_priv_key_pem, "", ""));
// 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_FALSE(status.ok());
ASSERT_TRUE(status.GetDetail().find(
"JWT algorithm 'rs512' is not matching with JWK algorithm 'rs256'")
!= std::string::npos)
<< " Actual error: " << status.GetDetail();
}
TEST(JwtUtilTest, VerifyJwtFailKeyNotFound) {
// The key cannot be found in JWKS.
TempTestDataFile jwks_file(Substitute(jwks_rsa_file_format, kid_1, "RS256",
rsa_pub_key_jwk_n, rsa_pub_key_jwk_e, kid_2, "RS256", rsa_invalid_pub_key_jwk_n,
rsa_pub_key_jwk_e));
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(rsa_pub_key_pem, rsa_priv_key_pem, "", ""));
// 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_FALSE(status.ok());
ASSERT_TRUE(
status.GetDetail().find("Invalid JWK ID in the JWT token") != std::string::npos)
<< " Actual error: " << status.GetDetail();
}
TEST(JwtUtilTest, VerifyJwtTokenWithoutKeyId) {
// Verify JWT token without key ID.
TempTestDataFile jwks_file(Substitute(jwks_rsa_file_format, kid_1, "RS256",
rsa_pub_key_jwk_n, rsa_pub_key_jwk_e, kid_2, "RS256", rsa_invalid_pub_key_jwk_n,
rsa_pub_key_jwk_e));
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(rsa_pub_key_pem, rsa_priv_key_pem, "", ""));
// 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(jwks_rsa_file_format, kid_1, "RS256",
rsa_pub_key_jwk_n, rsa_pub_key_jwk_e, kid_2, "RS256", rsa_invalid_pub_key_jwk_n,
rsa_pub_key_jwk_e));
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(rsa512_pub_key_pem, rsa512_priv_key_pem, "", ""));
// 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(jwks_rsa_file_format, kid_1, "RS256",
rsa_pub_key_jwk_n, rsa_pub_key_jwk_e, kid_2, "RS256", rsa_invalid_pub_key_jwk_n,
rsa_pub_key_jwk_e));
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_FALSE(status.ok());
ASSERT_TRUE(status.GetDetail().find("Unsecured JWT") != std::string::npos)
<< " Actual error: " << status.GetDetail();
}
TEST(JwtUtilTest, VerifyJwtFailExpiredToken) {
// Sign JWT token with RS256.
TempTestDataFile jwks_file(Substitute(jwks_rsa_file_format, kid_1, "RS256",
rsa_pub_key_jwk_n, rsa_pub_key_jwk_e, kid_2, "RS256", rsa_invalid_pub_key_jwk_n,
rsa_pub_key_jwk_e));
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(kid_1)
.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("impala"))
.sign(jwt::algorithm::rs256(rsa_pub_key_pem, rsa_priv_key_pem, "", ""));
// 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_FALSE(status.ok());
ASSERT_TRUE(status.GetDetail().find("Verification failed, error: token expired")
!= std::string::npos)
<< " Actual error: " << status.GetDetail();
}
} // namespace impala

595
be/src/util/jwt-util.cc Normal file
View File

@@ -0,0 +1,595 @@
// 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.
#include <string.h>
#include <cerrno>
#include <ostream>
#include <unordered_map>
#include <vector>
#include <sys/stat.h>
#include <boost/algorithm/string.hpp>
#include <gutil/strings/escaping.h>
#include <gutil/strings/substitute.h>
#include <openssl/bio.h>
#include <openssl/ec.h>
#include <openssl/evp.h>
#include <openssl/pem.h>
#include <openssl/rsa.h>
#include <rapidjson/document.h>
#include <rapidjson/error/en.h>
#include <rapidjson/filereadstream.h>
#include "common/names.h"
#include "jwt-util-internal.h"
#include "jwt-util.h"
namespace impala {
using rapidjson::Document;
using rapidjson::Value;
// JWK Set (JSON Web Key Set) is JSON data structure that represents a set of JWKs.
// This class parses JWKS file.
class JWKSetParser {
public:
JWKSetParser(JsonWebKeySet* jwks) : jwks_(jwks) {}
// Perform the parsing and populate JWKS's internal map. Return error status if
// encountering any error.
Status Parse(const Document& rules_doc) {
bool found_keys = false;
for (Value::ConstMemberIterator member = rules_doc.MemberBegin();
member != rules_doc.MemberEnd(); ++member) {
if (strcmp("keys", member->name.GetString()) == 0) {
found_keys = true;
RETURN_IF_ERROR(ParseKeys(member->value));
} else {
return Status(TErrorCode::JWKS_PARSE_ERROR,
Substitute(
"Unexpected property '$0' must be removed", member->name.GetString()));
}
}
if (!found_keys) {
return Status(TErrorCode::JWKS_PARSE_ERROR, "An array of keys is required");
}
return Status::OK();
}
private:
JsonWebKeySet* jwks_;
string NameOfTypeOfJsonValue(const Value& value) {
switch (value.GetType()) {
case rapidjson::kNullType:
return "Null";
case rapidjson::kFalseType:
case rapidjson::kTrueType:
return "Bool";
case rapidjson::kObjectType:
return "Object";
case rapidjson::kArrayType:
return "Array";
case rapidjson::kStringType:
return "String";
case rapidjson::kNumberType:
if (value.IsInt()) return "Integer";
if (value.IsDouble()) return "Float";
default:
DCHECK(false);
return "Unknown";
}
}
// Parse an array of keys.
Status ParseKeys(const Value& keys) {
if (!keys.IsArray()) {
return Status(TErrorCode::JWKS_PARSE_ERROR,
Substitute(
"'keys' must be of type Array but is a '$0'", NameOfTypeOfJsonValue(keys)));
} else if (keys.Size() == 0) {
return Status(
TErrorCode::JWKS_PARSE_ERROR, Substitute("'keys' must be a non empty Array"));
}
for (rapidjson::SizeType key_idx = 0; key_idx < keys.Size(); ++key_idx) {
const Value& key = keys[key_idx];
if (!key.IsObject()) {
return Status(TErrorCode::JWKS_PARSE_ERROR,
Substitute("parsing key #$0, key should be a JSON Object but is a '$1'.",
key_idx, NameOfTypeOfJsonValue(key)));
}
Status status = ParseKey(key);
if (!status.ok()) {
Status parse_status(
TErrorCode::JWKS_PARSE_ERROR, Substitute("parsing key #$0, ", key_idx));
parse_status.MergeStatus(status);
return parse_status;
}
}
return Status::OK();
}
// Parse a public key and populate JWKS's internal map.
Status ParseKey(const Value& json_key) {
std::unordered_map<std::string, std::string> kv_map;
string k, v;
for (Value::ConstMemberIterator member = json_key.MemberBegin();
member != json_key.MemberEnd(); ++member) {
k = string(member->name.GetString());
RETURN_IF_ERROR(ReadKeyProperty(k.c_str(), json_key, &v, /*required*/ false));
if (kv_map.find(k) == kv_map.end()) {
kv_map.insert(make_pair(k, v));
} else {
LOG(WARNING) << "Duplicate property of JWK: " << k;
}
}
auto it_kty = kv_map.find("kty");
if (it_kty == kv_map.end()) return Status("'kty' property is required");
auto it_kid = kv_map.find("kid");
if (it_kid == kv_map.end()) return Status("'kid' property is required");
string key_id = it_kid->second;
if (key_id.empty()) {
return Status(Substitute("'kid' property must be a non-empty string"));
}
Status status;
string key_type = boost::algorithm::to_lower_copy(it_kty->second);
if (key_type.compare("oct") == 0) {
JWTPublicKey* jwt_pub_key;
status = HSJWTPublicKeyBuilder::CreateJWKPublicKey(kv_map, &jwt_pub_key);
if (status.ok()) jwks_->AddHSKey(key_id, jwt_pub_key);
} else if (key_type.compare("rsa") == 0) {
JWTPublicKey* jwt_pub_key;
status = RSAJWTPublicKeyBuilder::CreateJWKPublicKey(kv_map, &jwt_pub_key);
if (status.ok()) jwks_->AddRSAPublicKey(key_id, jwt_pub_key);
} else {
return Status(Substitute("Unsupported kty: '$0'", key_type));
}
return status;
}
// Reads a key property of the given name and assigns the property value to the out
// parameter. A true return value indicates success.
template <typename T>
Status ReadKeyProperty(
const string& name, const Value& json_key, T* value, bool required = true) {
const Value& json_value = json_key[name.c_str()];
if (json_value.IsNull()) {
if (required) {
return Status(Substitute("'$0' property is required and cannot be null", name));
} else {
return Status::OK();
}
}
return ValidateTypeAndExtractValue(name, json_value, value);
}
// Extract a value stored in a rapidjson::Value and assign it to the out parameter.
// The type will be validated before extraction. A true return value indicates success.
// The name parameter is only used to generate an error message upon failure.
#define EXTRACT_VALUE(json_type, cpp_type) \
Status ValidateTypeAndExtractValue( \
const string& name, const Value& json_value, cpp_type* value) { \
if (!json_value.Is##json_type()) { \
return Status( \
Substitute("'$0' property must be of type " #json_type " but is a $1", name, \
NameOfTypeOfJsonValue(json_value))); \
} \
*value = json_value.Get##json_type(); \
return Status::OK(); \
}
EXTRACT_VALUE(String, string)
// EXTRACT_VALUE(Bool, bool)
};
//
// JWTPublicKey member functions.
//
// Verify JWT's signature for the given decoded token with jwt-cpp API.
Status JWTPublicKey::Verify(
const DecodedJWT& decoded_jwt, const std::string& algorithm) const {
// Verify if algorithms are matching.
if (algorithm_.compare(algorithm) != 0) {
return Status(TErrorCode::JWT_VERIFY_FAILED,
Substitute("JWT algorithm '$0' is not matching with JWK algorithm '$1'",
algorithm, algorithm_));
}
Status status;
try {
// Call jwt-cpp API to verify token's signature.
verifier_.verify(decoded_jwt);
} catch (const jwt::error::rsa_exception& e) {
status = Status(TErrorCode::JWT_VERIFY_FAILED, Substitute("RSA error: $0", e.what()));
} catch (const jwt::error::token_verification_exception& e) {
status = Status(TErrorCode::JWT_VERIFY_FAILED,
Substitute("Verification failed, error: $0", e.what()));
} catch (const std::exception& e) {
status = Status(TErrorCode::JWT_VERIFY_FAILED,
Substitute("Varification failed, error: $0", e.what()));
}
return status;
}
// Create a JWKPublicKey of HS from the JWK.
Status HSJWTPublicKeyBuilder::CreateJWKPublicKey(
JsonKVMap& kv_map, JWTPublicKey** pub_key_out) {
// Octet Sequence keys for HS256, HS384 or HS512.
// JWK Sample:
// {
// "kty":"oct",
// "alg":"HS256",
// "k":"f83OJ3D2xF1Bg8vub9tLe1gHMzV76e8Tus9uPHvRVEU",
// "kid":"Id that can be uniquely Identified"
// }
auto it_alg = kv_map.find("alg");
if (it_alg == kv_map.end()) return Status("'alg' property is required");
string algorithm = boost::algorithm::to_lower_copy(it_alg->second);
if (algorithm.empty()) {
return Status(Substitute("'alg' property must be a non-empty string"));
}
auto it_k = kv_map.find("k");
if (it_k == kv_map.end()) return Status("'k' property is required");
if (it_k->second.empty()) {
return Status(Substitute("'k' property must be a non-empty string"));
}
Status status;
JWTPublicKey* jwt_pub_key = nullptr;
try {
if (algorithm.compare("hs256") == 0) {
jwt_pub_key = new HS256JWTPublicKey(algorithm, it_k->second);
} else if (algorithm.compare("hs384") == 0) {
jwt_pub_key = new HS384JWTPublicKey(algorithm, it_k->second);
} else if (algorithm.compare("hs512") == 0) {
jwt_pub_key = new HS512JWTPublicKey(algorithm, it_k->second);
} else {
return Status(Substitute("Invalid 'alg' property value: '$0'", algorithm));
}
} catch (const std::exception& e) {
status = Status(TErrorCode::JWT_VERIFY_FAILED,
Substitute("Failed to initialize verifier, error: $0", e.what()));
}
if (!status.ok()) return status;
*pub_key_out = jwt_pub_key;
return Status::OK();
}
// Create a JWKPublicKey of RSA from the JWK.
Status RSAJWTPublicKeyBuilder::CreateJWKPublicKey(
JsonKVMap& kv_map, JWTPublicKey** pub_key_out) {
// JWK Sample:
// {
// "kty":"RSA",
// "alg":"RS256",
// "n":"sttddbg-_yjXzcFpbMJB1fI9...Q_QDhvqXx8eQ1r9smM",
// "e":"AQAB",
// "kid":"Id that can be uniquely Identified"
// }
auto it_alg = kv_map.find("alg");
if (it_alg == kv_map.end()) return Status("'alg' property is required");
string algorithm = boost::algorithm::to_lower_copy(it_alg->second);
if (algorithm.empty()) {
return Status(Substitute("'alg' property must be a non-empty string"));
}
auto it_n = kv_map.find("n");
auto it_e = kv_map.find("e");
if (it_n == kv_map.end() || it_e == kv_map.end()) {
return Status("'n' and 'e' properties are required");
} else if (it_n->second.empty() || it_e->second.empty()) {
return Status("'n' and 'e' properties must be a non-empty string");
}
// Converts public key to PEM encoded form.
string pub_key;
if (!ConvertJwkToPem(it_n->second, it_e->second, pub_key)) {
return Status(
Substitute("Invalid public key 'n':'$0', 'e':'$1'", it_n->second, it_e->second));
}
Status status;
JWTPublicKey* jwt_pub_key = nullptr;
try {
if (algorithm.compare("rs256") == 0) {
jwt_pub_key = new RS256JWTPublicKey(algorithm, pub_key);
} else if (algorithm.compare("rs384") == 0) {
jwt_pub_key = new RS384JWTPublicKey(algorithm, pub_key);
} else if (algorithm.compare("rs512") == 0) {
jwt_pub_key = new RS512JWTPublicKey(algorithm, pub_key);
} else {
return Status(Substitute("Invalid 'alg' property value: '$0'", algorithm));
}
} catch (const jwt::error::rsa_exception& e) {
status = Status(TErrorCode::JWT_VERIFY_FAILED, Substitute("RSA error: $0", e.what()));
} catch (const std::exception& e) {
status = Status(TErrorCode::JWT_VERIFY_FAILED,
Substitute("Failed to initialize verifier, error: $0", e.what()));
}
if (!status.ok()) return status;
*pub_key_out = jwt_pub_key;
return Status::OK();
}
// Convert public key of RSA from JWK format to PEM encoded format by using OpenSSL APIs.
bool RSAJWTPublicKeyBuilder::ConvertJwkToPem(
const std::string& base64_n, const std::string& base64_e, std::string& pub_key) {
pub_key.clear();
string str_n, str_e;
if (!WebSafeBase64Unescape(base64_n, &str_n)) return false;
if (!WebSafeBase64Unescape(base64_e, &str_e)) return false;
BIGNUM* modul = BN_bin2bn((const unsigned char*)str_n.c_str(), str_n.size(), nullptr);
BIGNUM* expon = BN_bin2bn((const unsigned char*)str_e.c_str(), str_e.size(), nullptr);
RSA* rsa = RSA_new();
#if OPENSSL_VERSION_NUMBER < 0x10100000L
rsa->n = modul;
rsa->e = expon;
#else
// RSA_set0_key is a new API introduced in OpenSSL version 1.1
RSA_set0_key(rsa, modul, expon, nullptr);
#endif
unsigned char desc[1024];
memset(desc, 0, 1024);
BIO* bio = BIO_new(BIO_s_mem());
PEM_write_bio_RSA_PUBKEY(bio, rsa);
if (BIO_read(bio, desc, 1024) > 0) {
pub_key = (char*)desc;
// Remove last '\n'.
if (pub_key.length() > 0 && pub_key[pub_key.length() - 1] == '\n') pub_key.pop_back();
}
BIO_free(bio);
RSA_free(rsa);
if (pub_key.empty()) return false;
return true;
}
//
// JsonWebKeySet member functions.
//
Status JsonWebKeySet::Init(const string& jwks_file_path) {
hs_key_map_.clear();
rsa_pub_key_map_.clear();
// Read the file.
FILE* jwks_file = fopen(jwks_file_path.c_str(), "r");
if (jwks_file == nullptr) {
return Status(
Substitute("Could not open JWKS file '$0'; $1", jwks_file_path, strerror(errno)));
}
// Check for an empty file and ignore it.
struct stat jwks_file_stats;
if (fstat(fileno(jwks_file), &jwks_file_stats)) {
fclose(jwks_file);
return Status(
Substitute("Error reading JWKS file '$0'; $1", jwks_file_path, strerror(errno)));
}
if (jwks_file_stats.st_size == 0) {
fclose(jwks_file);
return Status::OK();
}
char readBuffer[65536];
rapidjson::FileReadStream stream(jwks_file, readBuffer, sizeof(readBuffer));
Document jwks_doc;
jwks_doc.ParseStream(stream);
fclose(jwks_file);
if (jwks_doc.HasParseError()) {
return Status(
TErrorCode::JWKS_PARSE_ERROR, GetParseError_En(jwks_doc.GetParseError()));
}
if (!jwks_doc.IsObject()) {
return Status(TErrorCode::JWKS_PARSE_ERROR, "root element must be a JSON Object");
}
if (!jwks_doc.HasMember("keys")) {
return Status(TErrorCode::JWKS_PARSE_ERROR, "keys is required");
}
JWKSetParser jwks_parser(this);
return jwks_parser.Parse(jwks_doc);
}
void JsonWebKeySet::AddHSKey(std::string key_id, JWTPublicKey* jwk_pub_key) {
if (hs_key_map_.find(key_id) == hs_key_map_.end()) {
hs_key_map_[key_id].reset(jwk_pub_key);
} else {
LOG(WARNING) << "Duplicate key ID of JWK for HS key: " << key_id;
}
}
void JsonWebKeySet::AddRSAPublicKey(std::string key_id, JWTPublicKey* jwk_pub_key) {
if (rsa_pub_key_map_.find(key_id) == rsa_pub_key_map_.end()) {
rsa_pub_key_map_[key_id].reset(jwk_pub_key);
} else {
LOG(WARNING) << "Duplicate key ID of JWK for RSA public key: " << key_id;
}
}
const JWTPublicKey* JsonWebKeySet::LookupHSKey(const std::string& kid) const {
auto find_it = hs_key_map_.find(kid);
if (find_it == hs_key_map_.end()) {
// Could not find key for the given key ID.
return nullptr;
}
return find_it->second.get();
}
const JWTPublicKey* JsonWebKeySet::LookupRSAPublicKey(const std::string& kid) const {
auto find_it = rsa_pub_key_map_.find(kid);
if (find_it == rsa_pub_key_map_.end()) {
// Could not find key for the given key ID.
return nullptr;
}
return find_it->second.get();
}
//
// JWTHelper member functions.
//
struct JWTHelper::JWTDecodedToken {
JWTDecodedToken(const DecodedJWT& decoded_jwt) : decoded_jwt_(decoded_jwt) {}
DecodedJWT decoded_jwt_;
};
JWTHelper* JWTHelper::jwt_helper_ = new JWTHelper();
void JWTHelper::TokenDeleter::operator()(JWTHelper::JWTDecodedToken* token) const {
if (token != nullptr) delete token;
};
Status JWTHelper::Init(const std::string& jwks_file_path) {
jwks_.reset(new JsonWebKeySet());
RETURN_IF_ERROR(jwks_->Init(jwks_file_path));
if (jwks_->IsEmpty()) LOG(WARNING) << "JWKS file is empty.";
initialized_ = true;
return Status::OK();
}
// Decode the given JWT token.
Status JWTHelper::Decode(const string& token, UniqueJWTDecodedToken& decoded_token_out) {
Status status;
try {
// Call jwt-cpp API to decode the JWT token with default jwt::json_traits
// (jwt::picojson_traits).
decoded_token_out.reset(new JWTDecodedToken(jwt::decode(token)));
#ifndef NDEBUG
std::stringstream msg;
msg << "JWT token header: ";
for (auto& e : decoded_token_out.get()->decoded_jwt_.get_header_claims()) {
msg << e.first << "=" << e.second.to_json().serialize() << ";";
}
msg << " JWT token payload: ";
for (auto& e : decoded_token_out.get()->decoded_jwt_.get_payload_claims()) {
msg << e.first << "=" << e.second.to_json().serialize() << ";";
}
VLOG(3) << msg.str();
#endif
} catch (const std::invalid_argument& e) {
status = Status(TErrorCode::JWT_VERIFY_FAILED,
Substitute("Token is not in correct format, error: $0", e.what()));
} catch (const std::runtime_error& e) {
status = Status(TErrorCode::JWT_VERIFY_FAILED,
Substitute("Base64 decoding failed or invalid json, error: $0", e.what()));
}
return status;
}
// Validate the token's signature with public key.
Status JWTHelper::Verify(const JWTDecodedToken* decoded_token) const {
DCHECK(initialized_);
DCHECK(decoded_token != nullptr);
if (decoded_token->decoded_jwt_.get_signature().empty()) {
// Don't accept JWT without a signature.
return Status(TErrorCode::JWT_VERIFY_FAILED, "Unsecured JWT");
} else if (jwks_ == nullptr) {
// Skip to signature validation if there is no public key.
return Status::OK();
}
Status status;
try {
string algorithm =
boost::algorithm::to_lower_copy(decoded_token->decoded_jwt_.get_algorithm());
if (decoded_token->decoded_jwt_.has_key_id()) {
// Get key id from token's header and use it to retrieve the public key from JWKS.
std::string key_id = decoded_token->decoded_jwt_.get_key_id();
const JWTPublicKey* pub_key = nullptr;
if (algorithm.substr(0, 2).compare("hs") == 0) {
pub_key = jwks_->LookupHSKey(key_id);
} else if (algorithm.substr(0, 2).compare("rs") == 0) {
pub_key = jwks_->LookupRSAPublicKey(key_id);
} else {
return Status(TErrorCode::JWT_VERIFY_FAILED,
Substitute("Unsupported cryptographic algorithm '$0' for JWT", algorithm));
}
if (pub_key == nullptr) {
return Status(TErrorCode::JWT_VERIFY_FAILED, "Invalid JWK ID in the JWT token");
}
// Use the public key to verify the token's signature.
status = pub_key->Verify(decoded_token->decoded_jwt_, algorithm);
} else {
// According to RFC 7517 (JSON Web Key), 'kid' is OPTIONAL so it's possible there
// is no key id in the token's header. In this case, get all of public keys from
// JWKS for the family of algorithms.
const JsonWebKeySet::JWTPublicKeyMap* key_map = nullptr;
if (algorithm.substr(0, 2).compare("hs") == 0) {
key_map = jwks_->GetAllHSKeys();
} else if (algorithm.substr(0, 2).compare("rs") == 0) {
key_map = jwks_->GetAllRSAPublicKeys();
} else {
return Status(TErrorCode::JWT_VERIFY_FAILED,
Substitute("Unsupported cryptographic algorithm '$0' for JWT", algorithm));
}
if (key_map->size() == 0) {
return Status(
TErrorCode::JWT_VERIFY_FAILED, "Verification failed, no matching key");
}
// Try each key with matching algorithm util the signature is verified.
for (auto& key : *key_map) {
status = key.second->Verify(decoded_token->decoded_jwt_, algorithm);
if (status.ok()) return status;
}
}
} catch (const std::bad_cast& e) {
status = Status(TErrorCode::JWT_VERIFY_FAILED,
Substitute("Claim was present but not a string, error: $0", e.what()));
} catch (const jwt::error::claim_not_present_exception& e) {
status = Status(TErrorCode::JWT_VERIFY_FAILED,
Substitute("Claim not present in JWT token, error $0", e.what()));
} catch (const std::exception& e) {
status = Status(TErrorCode::JWT_VERIFY_FAILED,
Substitute("Token varification failed, error: $0", e.what()));
}
return status;
}
Status JWTHelper::GetCustomClaimUsername(const JWTDecodedToken* decoded_token,
const string& jwt_custom_claim_username, string& username) {
DCHECK(decoded_token != nullptr);
DCHECK(!jwt_custom_claim_username.empty());
Status status;
try {
// Get value of custom claim 'username' from the token payload.
if (decoded_token->decoded_jwt_.has_payload_claim(jwt_custom_claim_username)) {
// Assume the claim data type of 'username' is string.
username.assign(
decoded_token->decoded_jwt_.get_payload_claim(jwt_custom_claim_username)
.to_json()
.to_str());
if (username.empty()) {
status = Status(TErrorCode::JWT_VERIFY_FAILED,
Substitute("Claim '$0' is empty", jwt_custom_claim_username));
}
} else {
status = Status(TErrorCode::JWT_VERIFY_FAILED,
Substitute("Claim '$0' was not present", jwt_custom_claim_username));
}
} catch (const std::runtime_error& e) {
status = Status(TErrorCode::JWT_VERIFY_FAILED,
Substitute("Claim '$0' was not present, error: $1", jwt_custom_claim_username,
e.what()));
}
return status;
}
} // namespace impala

91
be/src/util/jwt-util.h Normal file
View File

@@ -0,0 +1,91 @@
// 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.
#ifndef IMPALA_JWT_UTIL_H
#define IMPALA_JWT_UTIL_H
#include <string>
#include "common/logging.h"
#include "common/status.h"
namespace impala {
class JsonWebKeySet;
/// JSON Web Token (JWT) is an Internet proposed standard for creating data with optional
/// signature and/or optional encryption whose payload holds JSON that asserts some
/// number of claims. The tokens are signed either using a private secret or a public/
/// private key.
/// This class works as wrapper for jwt-cpp. It provides APIs to decode/verify JWT token,
/// and extracts custom claim from the payload of JWT token.
/// JsonWebKeySet is read only after Init() is called. The class is thread safe.
class JWTHelper {
public:
/// Opaque types for storing the JWT decoded token. This allows us to avoid including
/// header file jwt-cpp/jwt.h.
struct JWTDecodedToken;
// Custom deleter: intended for use with std::unique_ptr<JWTDecodedToken>.
class TokenDeleter {
public:
/// Called by unique_ptr to free JWTDecodedToken
void operator()(JWTHelper::JWTDecodedToken* token) const;
};
/// UniqueJWTDecodedToken -- a wrapper around opaque decoded token structure to
/// facilitate automatic reference counting.
typedef std::unique_ptr<JWTDecodedToken, TokenDeleter> UniqueJWTDecodedToken;
/// Return the single instance.
static JWTHelper* GetInstance() { return jwt_helper_; }
/// Load JWKS from a JSON file. Returns an error if problems were encountered.
Status Init(const std::string& jwks_file_path);
/// Decode the given JWT token. The decoding result is stored in decoded_token_.
/// Return Status::OK if the decoding is successful.
static Status Decode(
const std::string& token, UniqueJWTDecodedToken& decoded_token_out);
/// Verify the token's signature with the given JWKS. The token should be already
/// decoded by calling Decode().
/// Return Status::OK if the verification is successful.
Status Verify(const JWTDecodedToken* decoded_token) const;
/// Extract custom claim "Username" from from the payload of the decoded JWT token.
/// Return Status::OK if the extraction is successful.
static Status GetCustomClaimUsername(const JWTDecodedToken* decoded_token,
const std::string& custom_claim_username, std::string& username);
/// Return Json Web Key Set. It's called only by unit-test code.
const JsonWebKeySet* GetJWKS() const { return jwks_.get(); }
private:
/// Single instance.
static JWTHelper* jwt_helper_;
/// Set it as TRUE when Init() is called.
bool initialized_ = false;
/// Json Web Key Set (JWKS) for Json Web Token (JWT) verification.
/// Only one instance per daemon.
std::unique_ptr<JsonWebKeySet> jwks_;
};
} // namespace impala
#endif

View File

@@ -41,8 +41,8 @@
#include "kudu/util/env.h"
#include "kudu/util/logging.h"
#include "kudu/util/net/sockaddr.h"
#include "rpc/authentication.h"
#include "rpc/authentication-util.h"
#include "rpc/authentication.h"
#include "rpc/thrift-util.h"
#include "runtime/exec-env.h"
#include "service/impala-server.h"
@@ -52,6 +52,7 @@
#include "util/cpu-info.h"
#include "util/debug-util.h"
#include "util/disk-info.h"
#include "util/jwt-util.h"
#include "util/mem-info.h"
#include "util/metrics.h"
#include "util/os-info.h"
@@ -150,6 +151,9 @@ DECLARE_string(ssl_minimum_version);
DECLARE_string(ssl_cipher_list);
DECLARE_string(trusted_domain);
DECLARE_bool(trusted_domain_use_xff_header);
DECLARE_bool(jwt_token_auth);
DECLARE_bool(jwt_validate_signature);
DECLARE_string(jwt_custom_claim_username);
static const char* DOC_FOLDER = "/www/";
static const int DOC_FOLDER_LEN = strlen(DOC_FOLDER);
@@ -282,7 +286,8 @@ Webserver::Webserver(const string& interface, const int port, MetricGroup* metri
error_handler_(UrlHandler(
bind<void>(&Webserver::ErrorHandler, this, _1, _2), "error.tmpl", false)),
use_cookies_(FLAGS_max_cookie_lifetime_s > 0),
check_trusted_domain_(!FLAGS_trusted_domain.empty()) {
check_trusted_domain_(!FLAGS_trusted_domain.empty()),
use_jwt_(FLAGS_jwt_token_auth) {
http_address_ = MakeNetworkAddress(interface.empty() ? "0.0.0.0" : interface, port);
Init();
@@ -309,6 +314,12 @@ Webserver::Webserver(const string& interface, const int port, MetricGroup* metri
total_trusted_domain_check_success_ =
metrics->AddCounter("impala.webserver.total-trusted-domain-check-success", 0);
}
if (use_jwt_) {
total_jwt_token_auth_success_ =
metrics->AddCounter("impala.webserver.total-jwt-token-auth-success", 0);
total_jwt_token_auth_failure_ =
metrics->AddCounter("impala.webserver.total-jwt-token-auth-failure", 0);
}
}
Webserver::~Webserver() {
@@ -600,8 +611,36 @@ sq_callback_result_t Webserver::BeginRequestCallback(struct sq_connection* conne
}
vector<string> response_headers;
bool authenticated = auth_mode_ != AuthMode::SPNEGO && auth_mode_ != AuthMode::LDAP;
// Try authenticating with a cookie first, if enabled.
bool authenticated = false;
// Try authenticating with JWT token first, if enabled.
if (use_jwt_) {
const char* auth_value = nullptr;
const char* value = sq_get_header(connection, "Authorization");
if (value != nullptr) auth_value = StripLeadingWhiteSpace(value);
// Check Authorization header with the Bearer authentication scheme as:
// Authorization: Bearer <token>
// A well-formed JWT consists of three concatenated Base64url-encoded strings,
// separated by dots (.).
if (auth_value != nullptr && strncasecmp(auth_value, "Bearer ", 7) == 0
&& strchr(auth_value, '.') != nullptr) {
string jwt_token = string(auth_value + 7);
StripWhiteSpace(&jwt_token);
if (!jwt_token.empty()) {
if (JWTTokenAuth(jwt_token, connection, request_info)) {
total_jwt_token_auth_success_->Increment(1);
authenticated = true;
// TODO: cookies are not added, but are not needed right now
} else {
LOG(INFO) << "Invalid JWT token provided: " << jwt_token;
total_jwt_token_auth_failure_->Increment(1);
}
}
}
}
if (!authenticated) {
authenticated = auth_mode_ != AuthMode::SPNEGO && auth_mode_ != AuthMode::LDAP;
}
// Try authenticating with a cookie, if enabled.
if (!authenticated && use_cookies_) {
const char* cookie_header = sq_get_header(connection, "Cookie");
string username;
@@ -832,6 +871,37 @@ bool Webserver::TrustedDomainCheck(const string& origin, struct sq_connection* c
return true;
}
bool Webserver::JWTTokenAuth(const std::string& jwt_token,
struct sq_connection* connection, struct sq_request_info* request_info) {
JWTHelper::UniqueJWTDecodedToken decoded_token;
Status status = JWTHelper::Decode(jwt_token, decoded_token);
if (!status.ok()) {
LOG(ERROR) << "Error decoding JWT token in Authorization header, "
<< "Error: " << status;
return false;
}
if (FLAGS_jwt_validate_signature) {
status = JWTHelper::GetInstance()->Verify(decoded_token.get());
if (!status.ok()) {
LOG(ERROR) << "Error verifying JWT token in Authorization header, "
<< "Error: " << status;
return false;
}
}
DCHECK(!FLAGS_jwt_custom_claim_username.empty());
string username;
status = JWTHelper::GetCustomClaimUsername(
decoded_token.get(), FLAGS_jwt_custom_claim_username, username);
if (!status.ok()) {
LOG(ERROR) << "Cannot retrieve username from JWT token in Authorization header, "
<< "Error: " << status;
return false;
}
request_info->remote_user = strdup(username.c_str());
return true;
}
Status Webserver::HandleBasic(struct sq_connection* connection,
struct sq_request_info* request_info, vector<string>* response_headers) {
const char* authz_header = sq_get_header(connection, "Authorization");

View File

@@ -201,6 +201,11 @@ class Webserver {
bool TrustedDomainCheck(const std::string& origin, struct sq_connection* connection,
struct sq_request_info* request_info);
/// Checks and returns true if the JWT token in Authorization header could be verified
/// and the token has a valid username.
bool JWTTokenAuth(const std::string& jwt_token, struct sq_connection* connection,
struct sq_request_info* request_info);
// Handle Basic authentication for this request. Returns an error if authentication was
// unsuccessful.
Status HandleBasic(struct sq_connection* connection,
@@ -267,6 +272,10 @@ class Webserver {
/// auth if it originates from a trusted domain.
bool check_trusted_domain_;
/// If true, the JWT token in Authorization header will be used for authentication.
/// An incoming connection will be accepted if the JWT token could be verified.
bool use_jwt_ = false;
/// Used to validate usernames/passwords If LDAP authentication is in use.
std::unique_ptr<ImpalaLdap> ldap_;
@@ -288,6 +297,11 @@ class Webserver {
/// If 'use_cookies_' is true, metrics for the number of successful
/// attempts to authorize connections originating from a trusted domain.
IntCounter* total_trusted_domain_check_success_ = nullptr;
/// If 'use_jwt_' is true, metrics for the number of successful and failed JWT auth
/// attempts.
IntCounter* total_jwt_token_auth_success_ = nullptr;
IntCounter* total_jwt_token_auth_failure_ = nullptr;
};
}

View File

@@ -438,7 +438,7 @@ def get_toolchain_downloads():
toolchain_packages += [llvm_package, llvm_package_asserts, gcc_package]
toolchain_packages += map(ToolchainPackage,
["avro", "binutils", "boost", "breakpad", "bzip2", "cctz", "cmake", "crcutil",
"flatbuffers", "gdb", "gflags", "glog", "gperftools", "gtest", "libev",
"flatbuffers", "gdb", "gflags", "glog", "gperftools", "gtest", "jwt-cpp", "libev",
"libunwind", "lz4", "openldap", "openssl", "orc", "protobuf", "python",
"rapidjson", "re2", "snappy", "thrift", "tpc-h", "tpc-ds", "zlib", "zstd"])
# Check whether this platform is supported (or whether a valid custom toolchain

View File

@@ -103,6 +103,8 @@ export IMPALA_GPERFTOOLS_VERSION=2.5-p4
unset IMPALA_GPERFTOOLS_URL
export IMPALA_GTEST_VERSION=1.6.0
unset IMPALA_GTEST_URL
export IMPALA_JWT_CPP_VERSION=0.5.0
unset IMPALA_JWT_CPP_URL
export IMPALA_LIBEV_VERSION=4.20
unset IMPALA_LIBEV_URL
export IMPALA_LIBUNWIND_VERSION=1.3-rc1-p3

View File

@@ -167,6 +167,7 @@ testdata/impala-profiles/impala_profile_log_tpcds_compute_stats_v2
testdata/impala-profiles/impala_profile_log_tpcds_compute_stats_v2_default.expected.txt
testdata/impala-profiles/impala_profile_log_tpcds_compute_stats_v2_extended.expected.txt
testdata/hive_benchmark/grepTiny/part-00000
testdata/jwt/jwks_rs256.json
testdata/tzdb/2017c.zip
testdata/tzdb/2017c-corrupt.zip
testdata/tzdb_tiny/*

View File

@@ -0,0 +1,38 @@
##############################################################################
# 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.
##############################################################################
# - Find jwt-cpp headers.
# JWT_CPP_ROOT hints the location
# This module defines JWT_CPP_INCLUDE_DIR, the directory containing headers
set(JWT_CPP_SEARCH_HEADER_PATHS ${JWT_CPP_ROOT}/include)
find_path(JWT_CPP_INCLUDE_DIR jwt-cpp/jwt.h HINTS
${JWT_CPP_SEARCH_HEADER_PATHS})
if (NOT JWT_CPP_INCLUDE_DIR)
message(FATAL_ERROR "jwt-cpp headers NOT found.")
set(JWT_CPP_FOUND FALSE)
else()
set(JWT_CPP_FOUND TRUE)
endif ()
mark_as_advanced(
JWT_CPP_INCLUDE_DIR
)

View File

@@ -471,6 +471,10 @@ error_codes = (
("LOCAL_DISK_FAULTY", 152,
"Query execution failure caused by local disk IO fatal error on backend: $0."),
("JWKS_PARSE_ERROR", 153, "Error parsing JWKS: $0."),
("JWT_VERIFY_FAILED", 154, "Error verifying JWT Token: $0."),
)
import sys

View File

@@ -1391,6 +1391,26 @@
"kind": "COUNTER",
"key": "impala.thrift-server.hiveserver2-http-frontend.total-saml-auth-failure"
},
{
"description": "The number of HiveServer2 HTTP API connection requests to this Impala Daemon that were successfully authenticated using JWT Token.",
"contexts": [
"IMPALAD"
],
"label": "HiveServer2 HTTP API Connection JWT Token Success",
"units": "NONE",
"kind": "COUNTER",
"key": "impala.thrift-server.hiveserver2-http-frontend.total-jwt-token-auth-success"
},
{
"description": "The number of HiveServer2 HTTP API connection requests to this Impala Daemon that were attempted to authenticate using JWT Token but were unsuccessful.",
"contexts": [
"IMPALAD"
],
"label": "HiveServer2 HTTP API Connection JWT Token Failure",
"units": "NONE",
"kind": "COUNTER",
"key": "impala.thrift-server.hiveserver2-http-frontend.total-jwt-token-auth-failure"
},
{
"description": "The amount of memory freed by the last memory tracker garbage collection.",
"contexts": [
@@ -2823,7 +2843,7 @@
"kind": "COUNTER",
"key": "impala.webserver.total-cookie-auth-failure"
},
{
{
"description": "The number of HTTP connection requests to this daemon's webserver that originated from a trusted domain.",
"contexts": [
"IMPALAD",
@@ -2835,6 +2855,30 @@
"kind": "COUNTER",
"key": "impala.webserver.total-trusted-domain-check-success"
},
{
"description": "The number of HTTP connection requests to this daemon's webserver that were successfully authenticated using a JWT token.",
"contexts": [
"IMPALAD",
"CATALOGSERVER",
"STATESTORE"
],
"label": "Webserver HTTP Connection JWT Token Auth Success",
"units": "NONE",
"kind": "COUNTER",
"key": "impala.webserver.total-jwt-token-auth-success"
},
{
"description": "The number of HTTP connection requests to this daemon's webserver that provided an invalid JWT token.",
"contexts": [
"IMPALAD",
"CATALOGSERVER",
"STATESTORE"
],
"label": "Webserver HTTP Connection JWT Token Auth Failure",
"units": "NONE",
"kind": "COUNTER",
"key": "impala.webserver.total-jwt-token-auth-failure"
},
{
"description": "The number of times the FAIL debug action returned an error. For testing only.",
"contexts": [

View File

@@ -0,0 +1,217 @@
// 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.
package org.apache.impala.customcluster;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import java.io.File;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.hive.service.rpc.thrift.*;
import org.apache.impala.util.Metrics;
import org.apache.thrift.transport.THttpClient;
import org.apache.thrift.protocol.TBinaryProtocol;
import org.junit.Before;
import org.junit.Test;
/**
* Tests that hiveserver2 operations over the http interface work as expected when
* JWT authentication is being used.
*/
public class JwtHttpTest {
Metrics metrics = new Metrics();
public void setUp(String extraArgs) throws Exception {
int ret = CustomClusterRunner.StartImpalaCluster(extraArgs);
assertEquals(ret, 0);
}
static void verifySuccess(TStatus status) throws Exception {
if (status.getStatusCode() == TStatusCode.SUCCESS_STATUS
|| status.getStatusCode() == TStatusCode.SUCCESS_WITH_INFO_STATUS) {
return;
}
throw new Exception(status.toString());
}
/**
* Executes 'query' and fetches the results. Expects there to be exactly one string
* returned, which be be equal to 'expectedResult'.
*/
static TOperationHandle execAndFetch(TCLIService.Iface client,
TSessionHandle sessionHandle, String query, String expectedResult)
throws Exception {
TExecuteStatementReq execReq = new TExecuteStatementReq(sessionHandle, query);
TExecuteStatementResp execResp = client.ExecuteStatement(execReq);
verifySuccess(execResp.getStatus());
TFetchResultsReq fetchReq = new TFetchResultsReq(
execResp.getOperationHandle(), TFetchOrientation.FETCH_NEXT, 1000);
TFetchResultsResp fetchResp = client.FetchResults(fetchReq);
verifySuccess(fetchResp.getStatus());
List<TColumn> columns = fetchResp.getResults().getColumns();
assertEquals(1, columns.size());
assertEquals(expectedResult, columns.get(0).getStringVal().getValues().get(0));
return execResp.getOperationHandle();
}
private void verifyJwtAuthMetrics(long expectedAuthSuccess, long expectedAuthFailure)
throws Exception {
long actualAuthSuccess =
(long) metrics.getMetric("impala.thrift-server.hiveserver2-http-frontend."
+ "total-jwt-token-auth-success");
assertEquals(expectedAuthSuccess, actualAuthSuccess);
long actualAuthFailure =
(long) metrics.getMetric("impala.thrift-server.hiveserver2-http-frontend."
+ "total-jwt-token-auth-failure");
assertEquals(expectedAuthFailure, actualAuthFailure);
}
/**
* Tests if sessions are authenticated by verifying the JWT token for connections
* to the HTTP hiveserver2 endpoint.
* Since we don't have Java version of JWT library, we use pre-calculated JWT token
* and JWKS. The token and JWK set used in this test case were generated by using
* BE unit-test function JwtUtilTest::VerifyJwtRS256.
*/
@Test
public void testJwtAuth() throws Exception {
String jwksFilename =
new File(System.getenv("IMPALA_HOME"), "testdata/jwt/jwks_rs256.json").getPath();
setUp(String.format(
"--jwt_token_auth=true --jwt_validate_signature=true --jwks_file_path=%s "
+ "--jwt_allow_without_tls=true",
jwksFilename));
THttpClient transport = new THttpClient("http://localhost:28000");
Map<String, String> headers = new HashMap<String, String>();
// Case 1: Authenticate with valid JWT Token in HTTP header.
String jwtToken =
"eyJhbGciOiJSUzI1NiIsImtpZCI6InB1YmxpYzpjNDI0YjY3Yi1mZTI4LTQ1ZDctYjAxNS1m"
+ "NzlkYTUwYjViMjEiLCJ0eXAiOiJKV1MifQ.eyJpc3MiOiJhdXRoMCIsInVzZXJuYW1lIjoia"
+ "W1wYWxhIn0.OW5H2SClLlsotsCarTHYEbqlbRh43LFwOyo9WubpNTwE7hTuJDsnFoVrvHiWI"
+ "02W69TZNat7DYcC86A_ogLMfNXagHjlMFJaRnvG5Ekag8NRuZNJmHVqfX-qr6x7_8mpOdU55"
+ "4kc200pqbpYLhhuK4Qf7oT7y9mOrtNrUKGDCZ0Q2y_mizlbY6SMg4RWqSz0RQwJbRgXIWSgc"
+ "bZd0GbD_MQQ8x7WRE4nluU-5Fl4N2Wo8T9fNTuxALPiuVeIczO25b5n4fryfKasSgaZfmk0C"
+ "oOJzqbtmQxqiK9QNSJAiH2kaqMwLNgAdgn8fbd-lB1RAEGeyPH8Px8ipqcKsPk0bg";
headers.put("Authorization", "Bearer " + jwtToken);
headers.put("X-Forwarded-For", "127.0.0.1");
transport.setCustomHeaders(headers);
transport.open();
TCLIService.Iface client = new TCLIService.Client(new TBinaryProtocol(transport));
// Open a session which will get username 'impala' from JWT token and use it as
// login user.
TOpenSessionReq openReq = new TOpenSessionReq();
TOpenSessionResp openResp = client.OpenSession(openReq);
// One successful authentication.
verifyJwtAuthMetrics(1, 0);
// Running a query should succeed.
TOperationHandle operationHandle = execAndFetch(
client, openResp.getSessionHandle(), "select logged_in_user()", "impala");
// Two more successful authentications - for the Exec() and the Fetch().
verifyJwtAuthMetrics(3, 0);
// case 2: Authenticate fails with invalid JWT token which does not have signature.
String invalidJwtToken =
"eyJhbGciOiJSUzI1NiIsImtpZCI6InB1YmxpYzpjNDI0YjY3Yi1mZTI4LTQ1ZDctYjAxNS1m"
+ "NzlkYTUwYjViMjEiLCJ0eXAiOiJKV1MifQ.eyJpc3MiOiJhdXRoMCIsInVzZXJuYW1lIjoia"
+ "W1wYWxhIn0.";
headers.put("Authorization", "Bearer " + invalidJwtToken);
headers.put("X-Forwarded-For", "127.0.0.1");
transport.setCustomHeaders(headers);
try {
openResp = client.OpenSession(openReq);
fail("Exception exception.");
} catch (Exception e) {
verifyJwtAuthMetrics(3, 1);
assertEquals(e.getMessage(), "HTTP Response code: 401");
}
// case 3: Authenticate fails without "Bearer" token.
headers.put("Authorization", "Basic VGVzdDFMZGFwOjEyMzQ1");
headers.put("X-Forwarded-For", "127.0.0.1");
transport.setCustomHeaders(headers);
try {
openResp = client.OpenSession(openReq);
fail("Exception exception.");
} catch (Exception e) {
// JWT authentication is not invoked.
verifyJwtAuthMetrics(3, 1);
assertEquals(e.getMessage(), "HTTP Response code: 401");
}
// case 4: Authenticate fails without "Authorization" header.
headers.put("X-Forwarded-For", "127.0.0.1");
transport.setCustomHeaders(headers);
try {
openResp = client.OpenSession(openReq);
fail("Exception exception.");
} catch (Exception e) {
// JWT authentication is not invoked.
verifyJwtAuthMetrics(3, 1);
assertEquals(e.getMessage(), "HTTP Response code: 401");
}
}
/**
* Tests if sessions are authenticated by verifying the JWT token for connections
* to the HTTP hiveserver2 endpoint.
*/
@Test
public void testJwtAuthNotVerifySig() throws Exception {
// Start Impala without jwt_validate_signature as false so that the signature of
// JWT token will not be validated.
setUp("--jwt_token_auth=true --jwt_validate_signature=false "
+ "--jwt_allow_without_tls=true");
THttpClient transport = new THttpClient("http://localhost:28000");
Map<String, String> headers = new HashMap<String, String>();
// Case 1: Authenticate with valid JWT Token in HTTP header.
// The Token was generated by BE unit-test function JwtUtilTest::VerifyJwtRS256.
String jwtToken =
"eyJhbGciOiJSUzI1NiIsImtpZCI6InB1YmxpYzpjNDI0YjY3Yi1mZTI4LTQ1ZDctYjAxNS1m"
+ "NzlkYTUwYjViMjEiLCJ0eXAiOiJKV1MifQ.eyJpc3MiOiJhdXRoMCIsInVzZXJuYW1lIjoia"
+ "W1wYWxhIn0.OW5H2SClLlsotsCarTHYEbqlbRh43LFwOyo9WubpNTwE7hTuJDsnFoVrvHiWI"
+ "02W69TZNat7DYcC86A_ogLMfNXagHjlMFJaRnvG5Ekag8NRuZNJmHVqfX-qr6x7_8mpOdU55"
+ "4kc200pqbpYLhhuK4Qf7oT7y9mOrtNrUKGDCZ0Q2y_mizlbY6SMg4RWqSz0RQwJbRgXIWSgc"
+ "bZd0GbD_MQQ8x7WRE4nluU-5Fl4N2Wo8T9fNTuxALPiuVeIczO25b5n4fryfKasSgaZfmk0C"
+ "oOJzqbtmQxqiK9QNSJAiH2kaqMwLNgAdgn8fbd-lB1RAEGeyPH8Px8ipqcKsPk0bg";
headers.put("Authorization", "Bearer " + jwtToken);
headers.put("X-Forwarded-For", "127.0.0.1");
transport.setCustomHeaders(headers);
transport.open();
TCLIService.Iface client = new TCLIService.Client(new TBinaryProtocol(transport));
// Open a session which will get username 'impala' from JWT token.
TOpenSessionReq openReq = new TOpenSessionReq();
TOpenSessionResp openResp = client.OpenSession(openReq);
// One successful authentication.
verifyJwtAuthMetrics(1, 0);
// Running a query should succeed.
TOperationHandle operationHandle = execAndFetch(
client, openResp.getSessionHandle(), "select logged_in_user()", "impala");
// Two more successful authentications - for the Exec() and the Fetch().
verifyJwtAuthMetrics(3, 0);
}
}

View File

@@ -0,0 +1,141 @@
// 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.
package org.apache.impala.customcluster;
import static org.apache.impala.testutil.LdapUtil.*;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.net.URLConnection;
import java.util.HashMap;
import java.util.Map;
import com.google.common.collect.Range;
import org.apache.impala.util.Metrics;
import org.junit.After;
import org.junit.Test;
/**
* Tests that Web Server works as expected when JWT authentication is being used.
*/
public class JwtWebserverTest {
private static final Range<Long> zero = Range.closed(0L, 0L);
Metrics metrics_ = new Metrics(TEST_USER_1, TEST_PASSWORD_1);
public void setUp(String extraArgs, String startArgs) throws Exception {
Map<String, String> env = new HashMap<>();
env.put("IMPALA_WEBSERVER_USERNAME", TEST_USER_1);
env.put("IMPALA_WEBSERVER_PASSWORD", TEST_PASSWORD_1);
int ret = CustomClusterRunner.StartImpalaCluster(extraArgs, env, startArgs);
assertEquals(ret, 0);
}
@After
public void cleanUp() throws IOException {
metrics_.Close();
}
private void verifyJwtAuthMetrics(
Range<Long> expectedAuthSuccess, Range<Long> expectedAuthFailure) throws Exception {
long actualAuthSuccess =
(long) metrics_.getMetric("impala.webserver.total-jwt-token-auth-success");
assertTrue("Expected: " + expectedAuthSuccess + ", Actual: " + actualAuthSuccess,
expectedAuthSuccess.contains(actualAuthSuccess));
long actualAuthFailure =
(long) metrics_.getMetric("impala.webserver.total-jwt-token-auth-failure");
assertTrue("Expected: " + expectedAuthFailure + ", Actual: " + actualAuthFailure,
expectedAuthFailure.contains(actualAuthFailure));
}
/**
* Tests if sessions are authenticated by verifying the JWT token for connections
* to the Web Server.
* Since we don't have Java version of JWT library, we use pre-calculated JWT token
* and JWKS. The token and JWK set used in this test case were generated by using
* BE unit-test function JwtUtilTest::VerifyJwtRS256.
*/
@Test
public void testWebserverJwtAuth() throws Exception {
String jwksFilename =
new File(System.getenv("IMPALA_HOME"), "testdata/jwt/jwks_rs256.json").getPath();
setUp(String.format(
"--jwt_token_auth=true --jwt_validate_signature=true --jwks_file_path=%s "
+ "--jwt_allow_without_tls=true",
jwksFilename),
"");
// Case 1: Authenticate with valid JWT Token in HTTP header.
String jwtToken =
"eyJhbGciOiJSUzI1NiIsImtpZCI6InB1YmxpYzpjNDI0YjY3Yi1mZTI4LTQ1ZDctYjAxNS1m"
+ "NzlkYTUwYjViMjEiLCJ0eXAiOiJKV1MifQ.eyJpc3MiOiJhdXRoMCIsInVzZXJuYW1lIjoia"
+ "W1wYWxhIn0.OW5H2SClLlsotsCarTHYEbqlbRh43LFwOyo9WubpNTwE7hTuJDsnFoVrvHiWI"
+ "02W69TZNat7DYcC86A_ogLMfNXagHjlMFJaRnvG5Ekag8NRuZNJmHVqfX-qr6x7_8mpOdU55"
+ "4kc200pqbpYLhhuK4Qf7oT7y9mOrtNrUKGDCZ0Q2y_mizlbY6SMg4RWqSz0RQwJbRgXIWSgc"
+ "bZd0GbD_MQQ8x7WRE4nluU-5Fl4N2Wo8T9fNTuxALPiuVeIczO25b5n4fryfKasSgaZfmk0C"
+ "oOJzqbtmQxqiK9QNSJAiH2kaqMwLNgAdgn8fbd-lB1RAEGeyPH8Px8ipqcKsPk0bg";
attemptConnection("Bearer " + jwtToken, "127.0.0.1");
verifyJwtAuthMetrics(Range.closed(1L, 1L), zero);
// Case 2: Failed with invalid JWT Token.
String invalidJwtToken =
"eyJhbGciOiJSUzI1NiIsImtpZCI6InB1YmxpYzpjNDI0YjY3Yi1mZTI4LTQ1ZDctYjAxNS1m"
+ "NzlkYTUwYjViMjEiLCJ0eXAiOiJKV1MifQ.eyJpc3MiOiJhdXRoMCIsInVzZXJuYW1lIjoia"
+ "W1wYWxhIn0.";
try {
attemptConnection("Bearer " + invalidJwtToken, "127.0.0.1");
} catch (IOException e) {
assertTrue(e.getMessage().contains("Server returned HTTP response code: 401"));
}
verifyJwtAuthMetrics(Range.closed(1L, 1L), Range.closed(1L, 1L));
// Case 3: Failed without "Bearer" token.
try {
attemptConnection("Basic VGVzdDFMZGFwOjEyMzQ1", "127.0.0.1");
} catch (IOException e) {
assertTrue(e.getMessage().contains("Server returned HTTP response code: 401"));
}
// JWT authentication is not invoked.
verifyJwtAuthMetrics(Range.closed(1L, 1L), Range.closed(1L, 1L));
// Case 4: Failed without "Authorization" header.
try {
attemptConnection(null, "127.0.0.1");
} catch (IOException e) {
assertTrue(e.getMessage().contains("Server returned HTTP response code: 401"));
}
// JWT authentication is not invoked.
verifyJwtAuthMetrics(Range.closed(1L, 1L), Range.closed(1L, 1L));
}
// Helper method to make a "get" call to the Web Server using the input JWT auth token
// and x-forward-for address.
private void attemptConnection(String auth_token, String xff_address) throws Exception {
String url = "http://localhost:25000/?json";
URLConnection connection = new URL(url).openConnection();
if (auth_token != null) connection.setRequestProperty("Authorization", auth_token);
if (xff_address != null) {
connection.setRequestProperty("X-Forwarded-For", xff_address);
}
connection.getInputStream();
}
}

View File

@@ -22,6 +22,7 @@ import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import java.io.File;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -123,6 +124,18 @@ public class LdapHS2Test {
assertEquals(expectedAuthSuccess, actualAuthSuccess);
}
private void verifyJwtAuthMetrics(long expectedAuthSuccess, long expectedAuthFailure)
throws Exception {
long actualAuthSuccess =
(long) metrics.getMetric("impala.thrift-server.hiveserver2-http-frontend."
+ "total-jwt-token-auth-success");
assertEquals(expectedAuthSuccess, actualAuthSuccess);
long actualAuthFailure =
(long) metrics.getMetric("impala.thrift-server.hiveserver2-http-frontend."
+ "total-jwt-token-auth-failure");
assertEquals(expectedAuthFailure, actualAuthFailure);
}
/**
* Tests LDAP authentication to the HTTP hiveserver2 endpoint.
*/
@@ -404,4 +417,66 @@ public class LdapHS2Test {
verifyMetrics(4, 1);
verifyTrustedDomainMetrics(6);
}
/**
* Tests if sessions are authenticated by verifying the JWT token for connections
* to the HTTP hiveserver2 endpoint.
*/
@Test
public void testHiveserver2JwtAuth() throws Exception {
String jwksFilename =
new File(System.getenv("IMPALA_HOME"), "testdata/jwt/jwks_rs256.json").getPath();
setUp(String.format(
"--jwt_token_auth=true --jwt_validate_signature=true --jwks_file_path=%s "
+ "--jwt_allow_without_tls=true",
jwksFilename));
verifyMetrics(0, 0);
THttpClient transport = new THttpClient("http://localhost:28000");
Map<String, String> headers = new HashMap<String, String>();
// Case 1: Authenticate with valid JWT Token in HTTP header.
String jwtToken =
"eyJhbGciOiJSUzI1NiIsImtpZCI6InB1YmxpYzpjNDI0YjY3Yi1mZTI4LTQ1ZDctYjAxNS1m"
+ "NzlkYTUwYjViMjEiLCJ0eXAiOiJKV1MifQ.eyJpc3MiOiJhdXRoMCIsInVzZXJuYW1lIjoia"
+ "W1wYWxhIn0.OW5H2SClLlsotsCarTHYEbqlbRh43LFwOyo9WubpNTwE7hTuJDsnFoVrvHiWI"
+ "02W69TZNat7DYcC86A_ogLMfNXagHjlMFJaRnvG5Ekag8NRuZNJmHVqfX-qr6x7_8mpOdU55"
+ "4kc200pqbpYLhhuK4Qf7oT7y9mOrtNrUKGDCZ0Q2y_mizlbY6SMg4RWqSz0RQwJbRgXIWSgc"
+ "bZd0GbD_MQQ8x7WRE4nluU-5Fl4N2Wo8T9fNTuxALPiuVeIczO25b5n4fryfKasSgaZfmk0C"
+ "oOJzqbtmQxqiK9QNSJAiH2kaqMwLNgAdgn8fbd-lB1RAEGeyPH8Px8ipqcKsPk0bg";
headers.put("Authorization", "Bearer " + jwtToken);
headers.put("X-Forwarded-For", "127.0.0.1");
transport.setCustomHeaders(headers);
transport.open();
TCLIService.Iface client = new TCLIService.Client(new TBinaryProtocol(transport));
// Open a session which will get username 'impala' from JWT token and use it as
// login user.
TOpenSessionReq openReq = new TOpenSessionReq();
TOpenSessionResp openResp = client.OpenSession(openReq);
// One successful authentication.
verifyMetrics(0, 0);
verifyJwtAuthMetrics(1, 0);
// Running a query should succeed.
TOperationHandle operationHandle = execAndFetch(
client, openResp.getSessionHandle(), "select logged_in_user()", "impala");
// Two more successful authentications - for the Exec() and the Fetch().
verifyMetrics(0, 0);
verifyJwtAuthMetrics(3, 0);
// case 2: Authenticate fails with invalid JWT token which does not have signature.
String invalidJwtToken =
"eyJhbGciOiJSUzI1NiIsImtpZCI6InB1YmxpYzpjNDI0YjY3Yi1mZTI4LTQ1ZDctYjAxNS1m"
+ "NzlkYTUwYjViMjEiLCJ0eXAiOiJKV1MifQ.eyJpc3MiOiJhdXRoMCIsInVzZXJuYW1lIjoia"
+ "W1wYWxhIn0.";
headers.put("Authorization", "Bearer " + invalidJwtToken);
headers.put("X-Forwarded-For", "127.0.0.1");
transport.setCustomHeaders(headers);
try {
openResp = client.OpenSession(openReq);
fail("Exception exception.");
} catch (Exception e) {
verifyJwtAuthMetrics(3, 1);
assertEquals(e.getMessage(), "HTTP Response code: 401");
}
}
}

View File

@@ -22,6 +22,7 @@ import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.net.URLConnection;
@@ -105,6 +106,18 @@ public class LdapWebserverTest {
expectedSuccess.contains(actualSuccess));
}
private void verifyJwtAuthMetrics(
Range<Long> expectedAuthSuccess, Range<Long> expectedAuthFailure) throws Exception {
long actualAuthSuccess =
(long) metrics_.getMetric("impala.webserver.total-jwt-token-auth-success");
assertTrue("Expected: " + expectedAuthSuccess + ", Actual: " + actualAuthSuccess,
expectedAuthSuccess.contains(actualAuthSuccess));
long actualAuthFailure =
(long) metrics_.getMetric("impala.webserver.total-jwt-token-auth-failure");
assertTrue("Expected: " + expectedAuthFailure + ", Actual: " + actualAuthFailure,
expectedAuthFailure.contains(actualAuthFailure));
}
@Test
public void testWebserver() throws Exception {
setUp("", "");
@@ -245,6 +258,45 @@ public class LdapWebserverTest {
verifyTrustedDomainMetrics(Range.closed(successMetricBefore, successMetricBefore));
}
/**
* Tests if sessions are authenticated by verifying the JWT token for connections
* to the Web Server.
*/
@Test
public void testWebserverJwtAuth() throws Exception {
String jwksFilename =
new File(System.getenv("IMPALA_HOME"), "testdata/jwt/jwks_rs256.json").getPath();
setUp(String.format(
"--jwt_token_auth=true --jwt_validate_signature=true --jwks_file_path=%s "
+ "--jwt_allow_without_tls=true",
jwksFilename),
"");
// Case 1: Authenticate with valid JWT Token in HTTP header.
String jwtToken =
"eyJhbGciOiJSUzI1NiIsImtpZCI6InB1YmxpYzpjNDI0YjY3Yi1mZTI4LTQ1ZDctYjAxNS1m"
+ "NzlkYTUwYjViMjEiLCJ0eXAiOiJKV1MifQ.eyJpc3MiOiJhdXRoMCIsInVzZXJuYW1lIjoia"
+ "W1wYWxhIn0.OW5H2SClLlsotsCarTHYEbqlbRh43LFwOyo9WubpNTwE7hTuJDsnFoVrvHiWI"
+ "02W69TZNat7DYcC86A_ogLMfNXagHjlMFJaRnvG5Ekag8NRuZNJmHVqfX-qr6x7_8mpOdU55"
+ "4kc200pqbpYLhhuK4Qf7oT7y9mOrtNrUKGDCZ0Q2y_mizlbY6SMg4RWqSz0RQwJbRgXIWSgc"
+ "bZd0GbD_MQQ8x7WRE4nluU-5Fl4N2Wo8T9fNTuxALPiuVeIczO25b5n4fryfKasSgaZfmk0C"
+ "oOJzqbtmQxqiK9QNSJAiH2kaqMwLNgAdgn8fbd-lB1RAEGeyPH8Px8ipqcKsPk0bg";
attemptConnection("Bearer " + jwtToken, "127.0.0.1");
verifyJwtAuthMetrics(Range.closed(1L, 1L), zero);
// Case 2: Failed with invalid JWT Token.
String invalidJwtToken =
"eyJhbGciOiJSUzI1NiIsImtpZCI6InB1YmxpYzpjNDI0YjY3Yi1mZTI4LTQ1ZDctYjAxNS1m"
+ "NzlkYTUwYjViMjEiLCJ0eXAiOiJKV1MifQ.eyJpc3MiOiJhdXRoMCIsInVzZXJuYW1lIjoia"
+ "W1wYWxhIn0.";
try {
attemptConnection("Bearer " + invalidJwtToken, "127.0.0.1");
} catch (IOException e) {
assertTrue(e.getMessage().contains("Server returned HTTP response code: 401"));
}
verifyJwtAuthMetrics(Range.closed(1L, 1L), Range.closed(1L, 1L));
}
// Helper method to make a get call to the webserver using the input basic
// auth token and x-forward-for token.
private void attemptConnection(String basic_auth_token, String xff_address)

7
testdata/jwt/jwks_rs256.json vendored Normal file
View File

@@ -0,0 +1,7 @@
{
"keys": [
{ "use": "sig", "kty": "RSA", "kid": "public:c424b67b-fe28-45d7-b015-f79da50b5b21", "alg": "RS256", "n": "uGbXWiK3dQTyCbX5xdE4yCuYp0AF2d15Qq1JSXT_lx8CEcXb9RbDddl8jGDv-spi5qPa8qEHiK7FwV2KpRE983wGPnYsAm9BxLFb4YrLYcDFOIGULuk2FtrPS512Qea1bXASuvYXEpQNpGbnTGVsWXI9C-yjHztqyL2h8P6mlThPY9E9ue2fCqdgixfTFIF9Dm4SLHbphUS2iw7w1JgT69s7of9-I9l5lsJ9cozf1rxrXX4V1u_SotUuNB3Fp8oB4C1fLBEhSlMcUJirz1E8AziMCxS-VrRPDM-zfvpIJg3JljAh3PJHDiLu902v9w-Iplu1WyoB2aPfitxEhRN0Yw", "e": "AQAB" },
{ "use": "sig", "kty": "RSA", "kid": "public:9b9d0b47-b9ed-4ba6-9180-52fc5b161a3a", "alg": "RS256", "n": "xzYuc22QSst_dS7geYYK5l5kLxU0tayNdixkEQ17ix-CUcUbKIsnyftZxaCYT46rQtXgCaYRdJcbB3hmyrOavkhTpX79xJZnQmfuamMbZBqitvscxW9zRR9tBUL6vdi_0rpoUwPMEh8-Bw7CgYR0FK0DhWYBNDfe9HKcyZEv3max8Cdq18htxjEsdYO0iwzhtKRXomBWTdhD5ykd_fACVTr4-KEY-IeLvubHVmLUhbE5NgWXxrRpGasDqzKhCTmsa2Ysf712rl57SlH0Wz_Mr3F7aM9YpErzeYLrl0GhQr9BVJxOvXcVd4kmY-XkiCcrkyS1cnghnllh-LCwQu1sYw", "e": "AQAB" }
]
}