blob: d8a0ca996c5694892705b6d2263fe5bbb89a7360 [file] [log] [blame]
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
#include "rpc/cookie-util.h"
#include <gutil/strings/escaping.h>
#include <gutil/strings/split.h>
#include <gutil/strings/strcat.h>
#include <gutil/strings/strip.h>
#include "util/network-util.h"
#include "util/openssl-util.h"
#include "util/string-parser.h"
DECLARE_bool(ldap_passwords_in_clear_ok);
DEFINE_int64(max_cookie_lifetime_s, 24 * 60 * 60,
"Maximum amount of time in seconds that an authentication cookie will remain valid. "
"Setting to 0 disables use of cookies. Defaults to 1 day.");
using namespace strings;
namespace impala {
// Used to separate values in cookies. All generated cookies will be of the form:
// <signature>&<username>&<timestamp>&<random number>
static const string COOKIE_SEPARATOR = "&";
// Cookies generated and processed by the HTTP server will be of the form:
// COOKIE_NAME=<cookie>
static const string COOKIE_NAME = "impala.auth";
// The maximum lenth for the base64 encoding of a SHA256 hash.
static const int SHA256_BASE64_LEN =
CalculateBase64EscapedLen(AuthenticationHash::HashLen(), /* do_padding */ true);
// Since we only return cookies with a single name, well behaved clients should only ever
// return one cookie to us. To accomodate non-malicious but poorly behaved clients, we
// allow for checking a limited number of cookies, up to MAX_COOKIES_TO_CHECK or until we
// find the first one with COOKIE_NAME.
static const int MAX_COOKIES_TO_CHECK = 5;
Status AuthenticateCookie(
const AuthenticationHash& hash, const string& cookie_header, string* username) {
// The 'Cookie' header allows sending multiple name/value pairs separated by ';'.
vector<string> cookies = strings::Split(cookie_header, ";");
if (cookies.size() > MAX_COOKIES_TO_CHECK) {
LOG(WARNING) << "Received cookie header with large number of cookies: "
<< cookie_header << ". Only checking the first " << MAX_COOKIES_TO_CHECK
<< " cookies.";
}
for (int i = 0; i < cookies.size() && i < MAX_COOKIES_TO_CHECK; ++i) {
string cookie_pair = cookies[i];
StripWhiteSpace(&cookie_pair);
string cookie;
if (!TryStripPrefixString(cookie_pair, StrCat(COOKIE_NAME, "="), &cookie)) {
continue;
}
if (cookie[0] == '"' && cookie[cookie.length() - 1] == '"') {
cookie = cookie.substr(1, cookie.length() - 2);
}
// Split the cookie into the signature and the cookie value.
vector<string> cookie_split = Split(cookie, delimiter::Limit(COOKIE_SEPARATOR, 1));
if (cookie_split.size() != 2) {
return Status("The cookie has an invalid format.");
}
const string& base64_signature = cookie_split[0];
const string& cookie_value = cookie_split[1];
string signature;
if (!WebSafeBase64Unescape(base64_signature, &signature)) {
return Status("Unable to decode base64 signature.");
}
if (signature.length() != AuthenticationHash::HashLen()) {
return Status("Signature is an incorrect length.");
}
bool verified = hash.Verify(reinterpret_cast<const uint8_t*>(cookie_value.data()),
cookie_value.length(), reinterpret_cast<const uint8_t*>(signature.data()));
if (!verified) {
return Status("The signature is incorrect.");
}
// Split the cookie value into username, timestamp, and random number.
vector<string> cookie_value_split = Split(cookie_value, COOKIE_SEPARATOR);
if (cookie_value_split.size() != 3) {
return Status("The cookie value has an invalid format.");
}
StringParser::ParseResult result;
int64_t create_time = StringParser::StringToInt<int64_t>(
cookie_value_split[1].c_str(), cookie_value_split[1].length(), &result);
if (result != StringParser::PARSE_SUCCESS) {
return Status("Could not parse cookie timestamp.");
}
// Check that the timestamp contained in the cookie is recent enough for the cookie
// to still be valid.
if (MonotonicMillis() - create_time <= FLAGS_max_cookie_lifetime_s * 1000) {
// We've successfully authenticated.
*username = cookie_value_split[0];
return Status::OK();
} else {
return Status("Cookie is past its max lifetime.");
}
}
return Status(Substitute("Did not find expected cookie name: $0", COOKIE_NAME));
}
string GenerateCookie(const string& username, const AuthenticationHash& hash) {
// Its okay to use rand() here even though its a weak RNG because being able to guess
// the random numbers generated won't help an attacker. The important thing is that
// we're using a strong RNG to create the key and a strong HMAC function.
string cookie_value =
StrCat(username, COOKIE_SEPARATOR, MonotonicMillis(), COOKIE_SEPARATOR, rand());
uint8_t signature[AuthenticationHash::HashLen()];
Status compute_status =
hash.Compute(reinterpret_cast<const uint8_t*>(cookie_value.data()),
cookie_value.length(), signature);
if (!compute_status.ok()) {
LOG(ERROR) << "Failed to compute cookie signature: " << compute_status;
return "";
}
DCHECK_EQ(SHA256_BASE64_LEN, 44);
char base64_signature[SHA256_BASE64_LEN + 1];
WebSafeBase64Escape(signature, AuthenticationHash::HashLen(), base64_signature,
SHA256_BASE64_LEN, /* do_padding */ true);
base64_signature[SHA256_BASE64_LEN] = '\0';
const char* secure_flag = ";Secure";
if (FLAGS_ldap_passwords_in_clear_ok) {
// If the user specified password can be sent without TLS/SSL, don't include the
// 'Secure' flag, which indicates the cookie should only be returned over secured
// connections. This is for testing only.
secure_flag = "";
}
return Substitute("$0=$1$2$3;HttpOnly;Max-Age=$4$5", COOKIE_NAME, base64_signature,
COOKIE_SEPARATOR, cookie_value, FLAGS_max_cookie_lifetime_s, secure_flag);
}
string GetDeleteCookie() {
return Substitute("$0=;HttpOnly;Max-Age=0", COOKIE_NAME);
}
} // namespace impala