Skip to content
Snippets Groups Projects
Commit f2c38a56 authored by Adrien Béraud's avatar Adrien Béraud
Browse files

server account manager: add OAuth token support

Gitlab: #226
Change-Id: I058c851795f912f261f371afe3edc2f9f09a9ff5
parent 2f043197
No related branches found
No related tags found
No related merge requests found
......@@ -121,7 +121,7 @@ public:
virtual void syncDevices() = 0;
virtual bool isPasswordValid(const std::string& password) { return false; };
virtual bool isPasswordValid(const std::string& /*password*/) { return false; };
dht::crypto::Identity loadIdentity(const std::string& crt_path, const std::string& key_path, const std::string& key_pwd) const;
......
......@@ -50,6 +50,7 @@ namespace jami {
constexpr const char* const QUERY_NAME {"/name/"};
constexpr const char* const QUERY_ADDR {"/addr/"};
constexpr const char* const CACHE_DIRECTORY {"namecache"};
constexpr const char DEFAULT_SERVER_HOST[] = "https://ns.jami.net";
const std::string HEX_PREFIX = "0x";
constexpr std::chrono::seconds SAVE_INTERVAL {5};
......@@ -68,6 +69,9 @@ toLower(std::string& string)
std::transform(string.begin(), string.end(), string.begin(), ::tolower);
}
NameDirectory&
NameDirectory::instance() { return instance(DEFAULT_SERVER_HOST); }
void
NameDirectory::lookupUri(const std::string& uri, const std::string& default_server,
LookupCallback cb)
......
......@@ -67,7 +67,11 @@ public:
void load();
static NameDirectory& instance(const std::string& serverUrl, std::shared_ptr<dht::Logger> l = {});
static NameDirectory& instance() { return instance(DEFAULT_SERVER_HOST); }
static NameDirectory& instance();
void setToken(std::string token) {
serverToken_ = std::move(token);
}
static void lookupUri(const std::string& uri, const std::string& default_server,
LookupCallback cb);
......@@ -83,9 +87,9 @@ private:
NON_COPYABLE(NameDirectory);
NameDirectory(NameDirectory&&) = delete;
NameDirectory& operator=(NameDirectory&&) = delete;
constexpr static const char* const DEFAULT_SERVER_HOST = "https://ns.jami.net";
std::string serverUrl_;
std::string serverToken_;
std::string cachePath_;
std::mutex cacheLock_ {};
......
......@@ -32,11 +32,11 @@ namespace jami {
using Request = dht::http::Request;
static const std::string PATH_AUTH = "/api/auth";
static const std::string PATH_LOGIN = "/api/login";
static const std::string PATH_AUTH = "/api/auth";
static const std::string PATH_DEVICE = PATH_AUTH + "/device";
static const std::string PATH_DEVICES = PATH_AUTH + "/devices";
constexpr const char* const HTTPS_PROTO {"https"};
static const std::string PATH_SEARCH = PATH_AUTH + "/directory/search";
ServerAccountManager::ServerAccountManager(
const std::string& path,
......@@ -52,10 +52,8 @@ ServerAccountManager::ServerAccountManager(
{};
void
ServerAccountManager::setHeaderFields(Request& request){
request.set_header_field(restinio::http_field_t::user_agent, "Jami");
request.set_header_field(restinio::http_field_t::accept, "application/json");
request.set_header_field(restinio::http_field_t::content_type, "application/json");
ServerAccountManager::setAuthHeaderFields(Request& request) const {
request.set_header_field(restinio::http_field_t::authorization, "Bearer " + token_);
}
void
......@@ -81,27 +79,19 @@ ServerAccountManager::initAuthentication(
onChange_ = std::move(onChange);
const std::string url = managerHostname_ + PATH_DEVICE;
JAMI_WARN("[Auth] authentication with: %s to %s", ctx->credentials->username.c_str(), url.c_str());
auto request = std::make_shared<Request>(*Manager::instance().ioContext(), url, logger_);
auto reqid = request->id();
request->set_method(restinio::http_method_post());
request->set_auth(ctx->credentials->username, ctx->credentials->password);
requests_[reqid] = request;
dht::ThreadPool::computation().run([onAsync = onAsync_, ctx, request, reqid]{
onAsync([ctx, request, reqid, onAsync](AccountManager& accountManager){
dht::ThreadPool::computation().run([onAsync = onAsync_, ctx, url]{
onAsync([=](AccountManager& accountManager){
auto& this_ = *static_cast<ServerAccountManager*>(&accountManager);
Json::Value body;
{
std::stringstream ss;
auto csr = ctx->request.get()->toString();
string_replace(csr, "\n", "\\n");
string_replace(csr, "\r", "\\r");
ss << "{\"csr\":\"" << csr << "\", \"deviceName\":\"" << ctx->deviceName << "\"}";
JAMI_WARN("[Auth] Sending request: %s", csr.c_str());
request->set_body(ss.str());
body["csr"] = csr;
body["deviceName"] = ctx->deviceName;
}
this_.setHeaderFields(*request);
request->add_on_done_callback([reqid, request, ctx, onAsync]
(const dht::http::Response& response){
auto request = std::make_shared<Request>(*Manager::instance().ioContext(), url, body, [ctx, onAsync]
(Json::Value json, const dht::http::Response& response){
JAMI_DBG("[Auth] Got request callback with status code=%u", response.status_code);
if (response.status_code == 0)
ctx->onFailure(AuthError::SERVER_ERROR, "Can't connect to server");
......@@ -112,22 +102,8 @@ ServerAccountManager::initAuthentication(
else {
do {
try {
Json::Value json;
std::string err;
Json::CharReaderBuilder rbuilder;
auto reader = std::unique_ptr<Json::CharReader>(rbuilder.newCharReader());
if (!reader->parse(response.body.data(), response.body.data() + response.body.size(), &json, &err)){
JAMI_ERR("[Auth] Can't parse server response: %s", err.c_str());
ctx->onFailure(AuthError::SERVER_ERROR, "Can't parse server response");
break;
}
JAMI_WARN("[Auth] Got server response: %s", response.body.c_str());
auto certStr = json["certificateChain"].asString();
string_replace(certStr, "\\n", "\n");
string_replace(certStr, "\\r", "\r");
auto cert = std::make_shared<dht::crypto::Certificate>(certStr);
auto cert = std::make_shared<dht::crypto::Certificate>(json["certificateChain"].asString());
auto accountCert = cert->issuer;
if (not accountCert) {
JAMI_ERR("[Auth] Can't parse certificate: no issuer");
......@@ -136,18 +112,14 @@ ServerAccountManager::initAuthentication(
}
auto receipt = json["deviceReceipt"].asString();
Json::Value receiptJson;
auto receiptReader = std::unique_ptr<Json::CharReader>(rbuilder.newCharReader());
std::string err;
auto receiptReader = std::unique_ptr<Json::CharReader>(Json::CharReaderBuilder{}.newCharReader());
if (!receiptReader->parse(receipt.data(), receipt.data() + receipt.size(), &receiptJson, &err)){
JAMI_ERR("[Auth] Can't parse receipt from server: %s", err.c_str());
ctx->onFailure(AuthError::SERVER_ERROR, "Can't parse receipt from server");
break;
}
onAsync([reqid, ctx,
json=std::move(json),
receiptJson=std::move(receiptJson),
cert,
accountCert,
receipt=std::move(receipt)] (AccountManager& accountManager) mutable
onAsync([=] (AccountManager& accountManager) mutable
{
auto& this_ = *static_cast<ServerAccountManager*>(&accountManager);
auto receiptSignature = base64::decode(json["receiptSignature"].asString());
......@@ -195,42 +167,131 @@ ServerAccountManager::initAuthentication(
}
} while (false);
}
onAsync([reqid](AccountManager& accountManager){
onAsync([response](AccountManager& accountManager){
auto& this_ = *static_cast<ServerAccountManager*>(&accountManager);
this_.requests_.erase(reqid);
this_.clearRequest(response.request);
});
});
request->send();
}, this_.logger_);
request->set_auth(ctx->credentials->username, ctx->credentials->password);
this_.sendRequest(request);
});
});
}
void
ServerAccountManager::authenticateDevice() {
const std::string url = managerHostname_ + PATH_LOGIN;
JAMI_WARN("[Auth] getting a device token: %s", url.c_str());
auto request = std::make_shared<Request>(*Manager::instance().ioContext(), url, Json::Value{Json::objectValue}, [onAsync = onAsync_] (Json::Value json, const dht::http::Response& response){
onAsync([=] (AccountManager& accountManager) {
auto& this_ = *static_cast<ServerAccountManager*>(&accountManager);
if (response.status_code >= 200 && response.status_code < 300) {
auto scopeStr = json["scope"].asString();
auto scope = scopeStr == "DEVICE" ? TokenScope::Device
: (scopeStr == "USER" ? TokenScope::User
: TokenScope::None);
JAMI_WARN("[Auth] Got server response: %d %s", response.status_code, response.body.c_str());
this_.setToken(json["access_token"].asString(), scope);
} else {
this_.authFailed(TokenScope::Device, response.status_code);
}
this_.clearRequest(response.request);
});
}, logger_);
request->set_identity(info_->identity);
// request->set_certificate_authority(info_->identity.second->issuer->issuer);
sendRequest(request);
}
void
ServerAccountManager::sendRequest(const std::shared_ptr<dht::http::Request>& request) {
request->set_header_field(restinio::http_field_t::user_agent, "Jami");
{
std::lock_guard<std::mutex> lock(requestLock_);
requests_.emplace(request);
}
request->send();
}
void
ServerAccountManager::clearRequest(const std::weak_ptr<dht::http::Request>& request) {
if (auto req = request.lock()) {
std::lock_guard<std::mutex> lock(requestLock_);
requests_.erase(req);
}
}
void
ServerAccountManager::authFailed(TokenScope scope, int code)
{
RequestQueue requests;
{
std::lock_guard<std::mutex> lock(tokenLock_);
requests = std::move(getRequestQueue(scope));
}
JAMI_DBG("[Auth] Failed auth with scope %d, ending %zu pending requests", (int)scope, requests.size());
while (not requests.empty()) {
auto req = std::move(requests.front());
requests.pop();
req->terminate(code == 0 ? asio::error::not_connected : asio::error::access_denied);
}
}
void
ServerAccountManager::setToken(std::string token, TokenScope scope)
{
std::lock_guard<std::mutex> lock(tokenLock_);
token_ = std::move(token);
nameDir_.get().setToken(token_);
if (not token_.empty()) {
auto& reqQueue = getRequestQueue(scope);
JAMI_DBG("[Auth] Got token with scope %d, handling %zu pending requests", (int)scope, reqQueue.size());
while (not reqQueue.empty()) {
auto req = std::move(reqQueue.front());
reqQueue.pop();
setAuthHeaderFields(*req);
sendRequest(req);
}
}
}
void ServerAccountManager::sendDeviceRequest(const std::shared_ptr<dht::http::Request>& req)
{
std::lock_guard<std::mutex> lock(tokenLock_);
if (hasAuthorization(TokenScope::Device)) {
setAuthHeaderFields(*req);
sendRequest(req);
} else {
auto& rQueue = getRequestQueue(TokenScope::Device);
if (rQueue.empty())
authenticateDevice();
rQueue.emplace(req);
}
}
void ServerAccountManager::sendAccountRequest(const std::shared_ptr<dht::http::Request>& req)
{
std::lock_guard<std::mutex> lock(tokenLock_);
if (hasAuthorization(TokenScope::User)) {
setAuthHeaderFields(*req);
sendRequest(req);
} else {
pendingAccountRequests_.emplace(req);
}
}
void
ServerAccountManager::syncDevices()
{
if (not creds_)
return;
const std::string url = managerHostname_ + PATH_DEVICES + "?username=" + creds_->username;
JAMI_WARN("[Auth] syncDevices with: %s to %s", creds_->username.c_str(), url.c_str());
auto request = std::make_shared<Request>(*Manager::instance().ioContext(), url, logger_);
auto reqid = request->id();
request->set_method(restinio::http_method_get());
request->set_auth(creds_->username, creds_->password);
setHeaderFields(*request);
request->add_on_done_callback([reqid, onAsync = onAsync_] (const dht::http::Response& response){
onAsync([reqid, response] (AccountManager& accountManager) {
const std::string url = managerHostname_ + PATH_DEVICES;
JAMI_WARN("[Auth] syncDevices %s", url.c_str());
sendDeviceRequest(std::make_shared<Request>(*Manager::instance().ioContext(), url, [onAsync = onAsync_] (Json::Value json, const dht::http::Response& response){
onAsync([=] (AccountManager& accountManager) {
JAMI_DBG("[Auth] Got request callback with status code=%u", response.status_code);
auto& this_ = *static_cast<ServerAccountManager*>(&accountManager);
if (response.status_code >= 200 || response.status_code < 300) {
if (response.status_code >= 200 && response.status_code < 300) {
try {
Json::Value json;
std::string err;
Json::CharReaderBuilder rbuilder;
auto reader = std::unique_ptr<Json::CharReader>(rbuilder.newCharReader());
if (!reader->parse(response.body.data(), response.body.data() + response.body.size(), &json, &err)){
JAMI_ERR("[Auth] Can't parse server response: %s", err.c_str());
return;
}
JAMI_WARN("[Auth] Got server response: %s", response.body.c_str());
if (not json.isArray()) {
JAMI_ERR("[Auth] Can't parse server response: not an array");
......@@ -248,11 +309,9 @@ ServerAccountManager::syncDevices()
JAMI_ERR("Error when loading device list: %s", e.what());
}
}
this_.requests_.erase(reqid);
this_.clearRequest(response.request);
});
});
request->send();
requests_[reqid] = std::move(request);
}, logger_));
}
bool
......@@ -265,27 +324,13 @@ ServerAccountManager::revokeDevice(const std::string& password, const std::strin
}
const std::string url = managerHostname_ + PATH_DEVICE + "?deviceId=" + device;
JAMI_WARN("[Revoke] Removing device of %s at %s", info_->username.c_str(), url.c_str());
auto request = std::make_shared<Request>(*Manager::instance().ioContext(), url, logger_);
auto reqid = request->id();
request->set_method(restinio::http_method_delete());
request->set_auth(info_->username, password);
setHeaderFields(*request);
request->add_on_done_callback([reqid, cb, onAsync = onAsync_] (const dht::http::Response& response){
onAsync([reqid, cb, response] (AccountManager& accountManager) {
auto request = std::make_shared<Request>(*Manager::instance().ioContext(), url, [cb, onAsync = onAsync_] (Json::Value json, const dht::http::Response& response){
onAsync([=] (AccountManager& accountManager) {
JAMI_DBG("[Revoke] Got request callback with status code=%u", response.status_code);
auto& this_ = *static_cast<ServerAccountManager*>(&accountManager);
if (response.status_code >= 200 || response.status_code < 300) {
if (response.body.empty())
return;
if (response.status_code >= 200 && response.status_code < 300) {
try {
Json::Value json;
std::string err;
Json::CharReaderBuilder rbuilder;
auto reader = std::unique_ptr<Json::CharReader>(rbuilder.newCharReader());
if (!reader->parse(response.body.data(), response.body.data() + response.body.size(), &json, &err)){
JAMI_ERR("[Revoke] Can't parse server response: %s", err.c_str());
}
JAMI_WARN("[Revoke] Got server response: %s", response.body.c_str());
JAMI_WARN("[Revoke] Got server response");
if (json["errorDetails"].empty()){
if (cb)
cb(RevokeDeviceResult::SUCCESS);
......@@ -298,12 +343,13 @@ ServerAccountManager::revokeDevice(const std::string& password, const std::strin
}
else if (cb)
cb(RevokeDeviceResult::ERROR_NETWORK);
this_.requests_.erase(reqid);
this_.clearRequest(response.request);
});
});
}, logger_);
request->set_method(restinio::http_method_delete());
request->set_auth(info_->username, password);
JAMI_DBG("[Revoke] Sending revoke device '%s' to JAMS", device.c_str());
request->send();
requests_[reqid] = std::move(request);
sendRequest(request);
return false;
}
......
......@@ -20,6 +20,9 @@
#include "account_manager.h"
#include <queue>
#include <set>
namespace jami {
class ServerAccountManager : public AccountManager {
......@@ -64,10 +67,42 @@ private:
const std::string managerHostname_;
std::shared_ptr<dht::Logger> logger_;
std::map<unsigned int /*id*/, std::shared_ptr<dht::http::Request>> requests_;
std::mutex requestLock_;
std::set<std::shared_ptr<dht::http::Request>> requests_;
std::unique_ptr<ServerAccountCredentials> creds_;
void setHeaderFields(dht::http::Request& request);
void sendRequest(const std::shared_ptr<dht::http::Request>& request);
void clearRequest(const std::weak_ptr<dht::http::Request>& request);
enum class TokenScope : unsigned {
None = 0,
Device,
User,
Admin
};
std::mutex tokenLock_;
TokenScope tokenScope_ {};
std::string token_ {};
using RequestQueue = std::queue<std::shared_ptr<dht::http::Request>>;
RequestQueue pendingDeviceRequests_;
RequestQueue pendingAccountRequests_;
RequestQueue& getRequestQueue(TokenScope scope) {
return scope == TokenScope::Device ? pendingDeviceRequests_ : pendingAccountRequests_;
}
bool hasAuthorization(TokenScope scope) const {
return not token_.empty() and tokenScope_ >= scope;
}
void setAuthHeaderFields(dht::http::Request& request) const;
void sendDeviceRequest(const std::shared_ptr<dht::http::Request>& req);
void sendAccountRequest(const std::shared_ptr<dht::http::Request>& req);
void authenticateDevice();
void authenticateAccount();
void authFailed(TokenScope scope, int code);
void setToken(std::string token, TokenScope scope);
};
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment