//------------------------------------------------------------------------------ // File: OAuth.cc // Author: Andreas-Joachim Peters - CERN //------------------------------------------------------------------------------ /************************************************************************ * EOS - the CERN Disk Storage System * * Copyright (C) 2019 CERN/Switzerland * * * * This program is free software: you can redistribute it and/or modify * * it under the terms of the GNU General Public License as published by * * the Free Software Foundation, either version 3 of the License, or * * (at your option) any later version. * * * * This program is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU General Public License for more details. * * * * You should have received a copy of the GNU General Public License * * along with this program. If not, see .* ************************************************************************/ #include "common/Logging.hh" #include "common/Namespace.hh" #include "common/OAuth.hh" #include "common/StringConversion.hh" #include "jwt-cpp/jwt.h" #include #include #include #include #include #include "common/Murmur3.hh" EOSCOMMONNAMESPACE_BEGIN; void OAuth::Init() { return; } std::size_t OAuth::callback(const char* in, std::size_t size, std::size_t num, std::string* out) { const std::size_t totalBytes(size * num); out->append(in, totalBytes); return totalBytes; } void OAuth::PurgeCache(time_t& now) { static time_t last_purge = time(NULL); eos::common::RWMutexWriteLock lock(mOAuthCacheMutex); // purge every 5 min if we have more than 64k entries or every hour for less if (((mOAuthInfo.size() > 65336) && (now - last_purge) > 300) || ((now - last_purge) > 3600)) { for (auto it = mOAuthInfo.begin(); it != mOAuthInfo.end();) { time_t ctime = strtoull(it->second["ctime"].c_str(), 0, 10); time_t etime = strtoull(it->second["etime"].c_str(), 0, 10); if ( (etime && (etime < now)) || ((now - ctime) > cache_validity_time)) { it = mOAuthInfo.erase(it); } else { ++it; } } } } int OAuth::Validate(OAuth::AuthInfo& info, const std::string& accesstoken, std::string resource, const std::string& refreshtoken, time_t& expires) { time_t now = time(NULL); if (expires && (expires < now)) { return ETIME; } try { // screen the audience auto decoded = jwt::decode(accesstoken); auto audiences = decoded.get_audience(); auto exp = decoded.get_expires_at(); auto iss = decoded.get_issuer(); std::string iss_resource = iss + "/protocol/openid-connect/userinfo"; if (resource.empty()) { // if we have a plain token without oauth2:token:resource wrapping, we build the resource from iss resource = iss_resource; } expires = std::chrono::system_clock::to_time_t(exp); bool audience_match = false; std::stringstream s; for (auto& e : decoded.get_payload_claims()) { s << e.first << "=" << e.second << " "; } eos_static_info("token='%s...' claims=[ %s ]", accesstoken.substr(0, 20).c_str(), s.str().c_str()); if (Mapping::IsOAuth2Resource(resource)) { // no audience require audience_match = true; } else { for (auto it = audiences.begin(); it != audiences.end(); ++it) { std::string audience_resource = resource + "@"; audience_resource += *it; if (Mapping::IsOAuth2Resource(audience_resource)) { audience_match = true; break; } } } if (!audience_match) { eos_static_err("msg=\"rejecing - no audience matches\""); return EPERM; } } catch (...) { eos_static_err("msg=\"rejecting - token decoding failed"); return EPERM; } // get the hash uint64_t tokenhash = Hash(accesstoken); PurgeCache(now); { eos::common::RWMutexReadLock lock(mOAuthCacheMutex); auto cache = mOAuthInfo.find(tokenhash); if (cache != mOAuthInfo.end()) { time_t ctime = strtoull(cache->second["ctime"].c_str(), 0, 10); time_t etime = strtoull(cache->second["etime"].c_str(), 0, 10); if ((!etime) || (etime > now)) { if ((now - ctime) < cache_validity_time) { info = cache->second; return 0; } } } } auto curl = curl_easy_init(); if (curl) { std::string httpsresource = std::string("https://") + resource; curl_easy_setopt(curl, CURLOPT_URL, httpsresource.c_str()); if (getenv("EOS_MGM_OIDC_INSECURE")) { curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0); curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0); } long httpCode(0); std::unique_ptr httpData(new std::string()); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, callback); curl_easy_setopt(curl, CURLOPT_WRITEDATA, httpData.get()); std::string auth = "Authorization: Bearer "; auth += accesstoken; auto chunk = curl_slist_append(NULL, auth.c_str()); curl_easy_setopt(curl, CURLOPT_HTTPHEADER, chunk); if (EOS_LOGS_DEBUG) { curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L); } curl_easy_perform(curl); curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &httpCode); curl_easy_cleanup(curl); if (httpCode == 200) { Json::Value jsonData; std::string errs; Json::CharReaderBuilder reader; std::unique_ptr jsonReader; jsonReader.reset(reader.newCharReader()); if (jsonReader->parse((*httpData.get()).c_str(), (*httpData.get()).c_str() + (*httpData.get()).length(), &jsonData, &errs)) { if (EOS_LOGS_DEBUG) { std::cerr << "Successfully parsed JSON data" << std::endl; std::cerr << "\nJSON data received:" << std::endl; std::cerr << jsonData.toStyledString() << std::endl; } if (jsonData.isMember("name")) { info["name"] = jsonData["name"].asString(); } if (jsonData.isMember("username")) { // OAuth style info["username"] = jsonData["username"].asString(); } else { if (getenv("EOS_MGM_OIDC_MAP_FIELD") && jsonData.isMember(getenv("EOS_MGM_OIDC_MAP_FIELD"))) { // configuration overwrite of field to map info["username"] = jsonData[getenv("EOS_MGM_OIDC_MAP_FIELD")].asString(); } else { // OIDC style if (jsonData.isMember("sub")) { info["username"] = jsonData["sub"].asString(); } else { // we need to have this field to map someone return EINVAL; } } } if (jsonData.isMember("email")) { info["email"] = jsonData["email"].asString(); } if (jsonData.isMember("federation")) { info["federation"] = jsonData["federation"].asString(); } // cache this entry info["ctime"] = std::to_string(time(NULL)); info["etime"] = expires ? std::to_string(expires) : std::to_string( now + cache_validity_time); eos::common::RWMutexWriteLock lock(mOAuthCacheMutex); mOAuthInfo[tokenhash] = info; return 0; } else { return EINVAL; } } else { return (int)httpCode; } } return EFAULT; } std::string OAuth::Handle(const std::string& info, eos::common::VirtualIdentity& vid) { std::vector tokens; eos::common::StringConversion::Tokenize(info, tokens, ":"); // this function handles now tokens of the form oauth2::[...] or just if (tokens.size() > 1) { if (tokens[0] == "oauth2") { if (tokens.size() < 3) { tokens.push_back(""); } if (tokens.size() < 4) { tokens.push_back("0"); } if (tokens.size() < 5) { tokens.push_back(""); } } } else { tokens.resize(5); tokens[0] = "oauth2"; tokens[1] = info; tokens[2] = ""; tokens[3] = "0"; tokens[4] = ""; } OAuth::AuthInfo oinfo; time_t expires = strtoull(tokens[3].c_str(), 0, 10); if (!Validate(oinfo, tokens[1], tokens[2], tokens[4], expires)) { // valid token, now map the user name eos_static_info("username='%s' name='%s' federation='%s' email='%s' expires=%llu", oinfo["username"].c_str(), oinfo["name"].c_str(), oinfo["federation"].c_str(), oinfo["email"].c_str(), expires); vid.federation = oinfo["federation"]; vid.email = oinfo["email"]; vid.fullname = oinfo["name"]; return oinfo["username"]; } return ""; } uint64_t OAuth::Hash(const std::string& token) { // std::cerr << "hashing token: " << token << std::endl; // std::cerr << "hash: " << Murmur3::MurmurHasher {}(token) << std::endl; return Murmur3::MurmurHasher {}(token); } EOSCOMMONNAMESPACE_END;