diff --git a/CMakeLists.txt b/CMakeLists.txt index 3e139c0..d45edc2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -4,6 +4,7 @@ project(j7s-mosquitto-plugin) include(external-deps.cmake) find_package(OpenSSL) +find_package(yaml-cpp) set(CMAKE_CXX_STANDARD 20) @@ -14,12 +15,19 @@ target_include_directories(utils PUBLIC ) target_link_libraries(utils OpenSSL::Crypto jwt-cpp) -add_library(j7s-plugin SHARED src/j7s-plugin.cpp src/AuthList.cpp src/Authorizer.cpp) +add_library(Authorizer SHARED src/Authorizer.cpp src/AuthList.cpp) +target_include_directories(Authorizer PUBLIC + $ + $ +) +target_link_libraries(Authorizer utils yaml-cpp) + +add_library(j7s-plugin SHARED src/j7s-plugin.cpp) target_include_directories(j7s-plugin PUBLIC $ $ ) -target_link_libraries(j7s-plugin utils) +target_link_libraries(j7s-plugin utils Authorizer) add_executable(gen-token src/gen-token.cpp) target_include_directories(gen-token PUBLIC diff --git a/README.md b/README.md index 04fc17c..030363d 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ Authentication using JWTs for the mosquitto mqtt broker. ## Dependencies ``` -sudo apt install mosquitto-dev g++ cmake libmosquitto-dev mosquitto-clients openssl libssl-dev googletest +sudo apt install mosquitto-dev g++ cmake libmosquitto-dev mosquitto-clients openssl libssl-dev libyaml-cpp-dev ``` ## Generating offline keys diff --git a/examples/acl.yaml b/examples/acl.yaml new file mode 100644 index 0000000..f30bdea --- /dev/null +++ b/examples/acl.yaml @@ -0,0 +1,6 @@ +default: + can_read: true + can_write: false +jimmy: + can_read: true + can_write: true \ No newline at end of file diff --git a/examples/mosquitto.conf b/examples/mosquitto.conf index 9133de9..d2e8205 100644 --- a/examples/mosquitto.conf +++ b/examples/mosquitto.conf @@ -6,12 +6,14 @@ protocol websockets allow_anonymous false auth_plugin /home/jimmy/Develop/mosquitto-plugin/build/libj7s-plugin.so auth_opt_issuer https://auth.jpace121.net/realms/jpace121-main -auth_opt_public_key /home/jimmy/Develop/mosquitto-plugin/test/key.pem +auth_opt_public_key /home/jimmy/Develop/mosquitto-plugin/examples/key.pem +auth_opt_acl_file /home/jimmy/Develop/mosquitto-plugin/examples/acl.yaml listener 8081 protocol mqtt allow_anonymous false auth_plugin /home/jimmy/Develop/mosquitto-plugin/build/libj7s-plugin.so auth_opt_issuer https://auth.jpace121.net/realms/jpace121-main -auth_opt_public_key /home/jimmy/Develop/mosquitto-plugin/test/key.pem +auth_opt_public_key /home/jimmy/Develop/mosquitto-plugin/examples/key.pem +auth_opt_acl_file /home/jimmy/Develop/mosquitto-plugin/examples/acl.yaml diff --git a/include/j7s-plugin/AuthList.hpp b/include/j7s-plugin/AuthList.hpp index 9b8e396..f412d91 100644 --- a/include/j7s-plugin/AuthList.hpp +++ b/include/j7s-plugin/AuthList.hpp @@ -12,19 +12,22 @@ // See the License for the specific language governing permissions and // limitations under the License. #pragma once -#include +#include +#include #include +using time_T = std::chrono::time_point; + // A list with easily checkable contents. class AuthList { public: AuthList(); - void add(const std::string& username); + void add(const std::string& username, const time_T& expr_time); void remove(const std::string& username); bool confirm(const std::string& username); private: - std::forward_list _allowedUsernames; + std::map _map; }; diff --git a/include/j7s-plugin/Authorizer.hpp b/include/j7s-plugin/Authorizer.hpp index 99ae445..64275cc 100644 --- a/include/j7s-plugin/Authorizer.hpp +++ b/include/j7s-plugin/Authorizer.hpp @@ -15,6 +15,8 @@ #include +#include + #include #include @@ -22,7 +24,7 @@ class Authorizer { public: - Authorizer(const std::string& pub_key, const std::string& issuer); + Authorizer(const std::string& pub_key, const std::string& issuer, const std::string& aclFilePath); static std::optional read_key(const std::string& key_file); void add_unknown(const std::string& username); bool is_unknown(const std::string& username); @@ -35,6 +37,8 @@ private: AuthList _readList; AuthList _unknownList; + YAML::Node _aclFile; + std::string _pub_key; std::string _issuer; }; diff --git a/include/j7s-plugin/utils.h b/include/j7s-plugin/utils.h index 1d9dc34..cdfb1d5 100644 --- a/include/j7s-plugin/utils.h +++ b/include/j7s-plugin/utils.h @@ -19,7 +19,7 @@ std::optional read_key(const std::string & key_file); -bool validate( +std::tuple> validate( const std::string & token, const std::string & username, const std::string & issuer, diff --git a/src/AuthList.cpp b/src/AuthList.cpp index e1030de..42bc61e 100644 --- a/src/AuthList.cpp +++ b/src/AuthList.cpp @@ -14,36 +14,38 @@ #include #include -AuthList::AuthList() : _allowedUsernames{} {} +AuthList::AuthList() : _map{} {} -void AuthList::add(const std::string & username) +void AuthList::add(const std::string & username, const time_T& expr_time) { - // Is the username already in the list? - // If not add it. - if (not confirm(username)) - { - _allowedUsernames.emplace_front(username); - } + // Add the user to the list or update it's expr time if + // it's already there. + _map[username] = expr_time; } void AuthList::remove(const std::string & username) { - // Is the user in the list? - // Is so, remove it, - if (confirm(username)) - { - _allowedUsernames.remove(username); - } + // Remove the user + _map.erase(username); } bool AuthList::confirm(const std::string & username) { - // Is the user in the list? - const auto found = - std::find(std::begin(_allowedUsernames), std::end(_allowedUsernames), username); - if (found != std::end(_allowedUsernames)) + // Is the user in the map? + const auto iter = _map.find(username); + + if(iter == _map.end()) + { + return false; + } + + // Has the token expired? + const auto now = std::chrono::system_clock::now(); + const auto expr_time = std::get<1>(*iter); + if(now < expr_time) { return true; } + return false; } diff --git a/src/Authorizer.cpp b/src/Authorizer.cpp index e74e2e5..e765072 100644 --- a/src/Authorizer.cpp +++ b/src/Authorizer.cpp @@ -17,14 +17,21 @@ #include #include -Authorizer::Authorizer(const std::string & pub_key, const std::string & issuer) : - _pub_key{pub_key}, _issuer{issuer} +#include + +// Util. +std::tuple checkACL(const YAML::Node& user); + +// Class implementation. +Authorizer::Authorizer( + const std::string & pub_key, const std::string & issuer, const std::string & aclFilePath) : + _pub_key{pub_key}, _issuer{issuer}, _aclFile{aclFilePath} { } void Authorizer::add_unknown(const std::string & username) { - _unknownList.add(username); + _unknownList.add(username, time_T::max()); } bool Authorizer::is_unknown(const std::string & username) @@ -34,20 +41,35 @@ bool Authorizer::is_unknown(const std::string & username) bool Authorizer::add(const std::string & token, const std::string & username) { - const auto validated = validate(token, username, _issuer, _pub_key); + const auto [validated, expr_time] = validate(token, username, _issuer, _pub_key); if (not validated) { std::cerr << "Not validated." << std::endl; return false; } - // TODO: Check ACL file to see which one. - _writeList.add(username); - _readList.add(username); + // Check the ACL file. + // TODO: Make sure default is in ACL file. + if (not _aclFile[username]) + { + const auto checkACL(_aclFile["default"]); + return true; + } + const auto [can_read, can_write] = checkACL(_aclFile[username]); + + if (can_read) + { + _readList.add(username, expr_time); + } + if (can_write) + { + _writeList.add(username, expr_time); + } return true; } + bool Authorizer::can_read(const std::string & username) { return _readList.confirm(username); @@ -64,3 +86,20 @@ void Authorizer::logout(const std::string & username) _readList.remove(username); _unknownList.remove(username); } + +// Util. +std::tuple checkACL(const YAML::Node& user) +{ + bool can_read = false; + bool can_write = false; + if(user["can_read"] and user["can_read"].as()) + { + can_read = true; + } + if(user["can_write"] and user["can_write"].as()) + { + can_write = true; + } + + return std::make_tuple(can_read, can_write); +} diff --git a/src/j7s-plugin.cpp b/src/j7s-plugin.cpp index 6fc3658..32cbd53 100644 --- a/src/j7s-plugin.cpp +++ b/src/j7s-plugin.cpp @@ -14,8 +14,12 @@ #include #include + +#include + #include #include +#include // Mosquitto Globals static mosquitto_plugin_id_t * plugin_id = nullptr; @@ -41,7 +45,7 @@ int mosquitto_plugin_init( { plugin_id = identifier; - if (option_count != 2) + if (option_count < 3) { mosquitto_log_printf(MOSQ_LOG_ERR, "Missing an option. Found: %d", option_count); return MOSQ_ERR_INVAL; @@ -49,12 +53,13 @@ int mosquitto_plugin_init( std::string public_key; std::string issuer; + std::filesystem::path aclFilePath; for (int index = 0; index < option_count; index++) { const auto key = std::string(options[index].key); if (key == "public_key") { - const auto key = Authorizer::read_key(std::string(options[index].value)); + const auto key = read_key(std::string(options[index].value)); if (not key or key->empty()) { mosquitto_log_printf(MOSQ_LOG_ERR, "Could not read public key."); @@ -71,9 +76,20 @@ int mosquitto_plugin_init( return MOSQ_ERR_INVAL; } } + else if (key == "acl_file") + { + std::string acl_file_string = std::string(options[index].value); + if (acl_file_string.empty()) + { + mosquitto_log_printf(MOSQ_LOG_ERR, "acl_file not set."); + return MOSQ_ERR_INVAL; + } + aclFilePath = std::filesystem::path(acl_file_string); + } } - authorizer = std::make_unique(public_key, issuer); + authorizer = std::make_unique( + public_key, issuer, std::filesystem::absolute(aclFilePath).string()); // Register the callbacks. mosquitto_callback_register( diff --git a/src/utils.cpp b/src/utils.cpp index 75e073c..7874c02 100644 --- a/src/utils.cpp +++ b/src/utils.cpp @@ -32,7 +32,7 @@ std::optional read_key(const std::string & key_file) return ss.str(); } -bool validate( +std::tuple> validate( const std::string & token, const std::string & username, const std::string & issuer, @@ -50,7 +50,7 @@ bool validate( catch (jwt::error::token_verification_exception & exception) { std::cerr << exception.what() << std::endl; - return false; + return std::make_tuple(false, std::chrono::system_clock::now()); } auto claims = decoded_token.get_payload_claims(); @@ -58,22 +58,34 @@ bool validate( if (not claims.contains("upn")) { std::cerr << "Missing upn." << std::endl; - return false; + return std::make_tuple(false, std::chrono::system_clock::now()); } if (claims["upn"].as_string() != username) { std::cerr << "Wrong username." << std::endl; - return false; + return std::make_tuple(false, std::chrono::system_clock::now()); } // Check for mqtt-write claim value. if (not claims.contains("mqtt")) { std::cerr << "Missing mqtt claim." << std::endl; - return false; + return std::make_tuple(false, std::chrono::system_clock::now()); + } + if(not claims["mqtt"].as_bool()) + { + std::cerr << "Not claiming can do mqtt." << std::endl; + return std::make_tuple(false, std::chrono::system_clock::now()); } - return claims["mqtt"].as_bool(); + // Do we have an expiration time? + if(not claims.contains("exp")) + { + std::cerr << "Missing expiration time claim." << std::endl; + return std::make_tuple(false, std::chrono::system_clock::now()); + } + + return std::make_tuple(true, claims["exp"].as_date()); } std::string gen_token( diff --git a/test/token_test.cpp b/test/token_test.cpp index c6ef489..21911b1 100644 --- a/test/token_test.cpp +++ b/test/token_test.cpp @@ -1,10 +1,104 @@ -#include +// Copyright 2022 James Pace +// +// Licensed 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 #include "gtest/gtest.h" +constexpr std::string priv_key_a = + R"(-----BEGIN PRIVATE KEY----- +MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQC+ouwDpYOWDEyM +nJhwejOn+boDxw4ntiOR3kRzIANuJrbEPf3UJFL+SPPzzY7NU1A6XPz/NAccbvfn +c78dj12rsV6st5GuFx9QbxYn2XQb8vnxj+DhvSrNk+qy7IMaN/3NGrAoWemSIRIW +VB7xbVybQyvAucgaTDKnU72viNOxqg8v5bGF+WtTjKwezmYtyQ8Z7dpGQbML1tkT +EQwTq5nnLre8F/t6fTS4ziGVw7STggSroAHazphzYmqc3W68jY/SQefOilALwzFp +/Cxoubj0d+f3OYT5jnfMPSpKJiYNlLqxCJPGjNcSRxjzzRt/cRYzhAPfriO/fkYG +tQcLNB5dAgMBAAECggEAd+qyPeT6rgNUj8rdlTs5jTtoiIHJZK+NFm/TbPvBTKPr +qew45B5pWm13j3BJmN0EhYIC32HR60/ef2hu2uBZEuyC2nCqofEHkKggLrb5867X +DN3tnvJIn4KhSyW9nluEOmXEU82jQHmvD/6gbEvXyg7p0dTLi8dMwbbKhkWyrHlu +lqvuJUvdDFv9X2k/y440cKhyssP5HlR/sXn+za5XQoPEtZIh9xM9sg0slSIq+eu1 +FRKS0Geo8e93L31jXn1GoNTSCIupyj3EZiKGE0xhxTmjoO+dEEVg6gTdYNAQd6Nx +aaMdLRNo2hfk7ATA+L3hcfFSM+3QPg7wFCInGHQF/QKBgQD1aQ+GX6vl3lmZs+TX +6Hp7qtL6g+TJ2/fSXqbMURHBtdTFFzROqtzIAHwp30fGCGG9reAmRZVHv2mF7U49 +3qk9/TcK4nUsGq/o87RKjmrUmLrEx1mtJK10BuJW2lEPIBG6Ws9tGAwSzhs5Lw5H +LnbQHD4dftjhqhNX8ZoU5oG7dwKBgQDG3MwqaMQ55sh8+ci6tZ4pOm1/8Lin0gyh +iNFa8UxFkTsaLHnDXrsUJCkqRwtNtV4Fhbv7x+4smGxDzuJkF6U7uxONJgWp1qlW +6B0SBgKUPdxeGJYG4+ww9qsapARZzZ/1GLYv47+kPs0slz+A0OHeNs1BKhGJLK23 +P88MSG8BywKBgFnLs26Lmy5lCYwAEwAdhJOzkbcwg4qI/kjvcUDZeRHUIqJrNyyB +wH8+DjCUDoMblgf9k0Ltuw2hsE7c4gApdOvFt1o4On+E1FD8uz98lQJtUAmol9uO +zBjkW/VDtN0/8rypdbSJVAGdgMCPwz2wdrD3ZJMOUvVfcex/7s0u+tFJAoGAJoPb +ExepcaFuES57nxXP5SJI1O+1g+NdyOdrzNZRNGQVc1NL3ff5+cOrKWILIWjQJfep +2fD2AzMePN/T3xjpSrFH7x1/GU7XC1r3TmdVloqIpLzUSc9ZDn6n0wgTQ6Vcpqa7 +mnjcxB3ZtRoyFWvfYx9wD3/rV4sMtiIoorNgtJMCgYABDGH571InLE9HMO1+Czmp +zyvcbTAq8GiN0G4Rok95+THfa726N6BcmkZUK1xWaleO6xNGrDsBghfmgw629Ujk +UJ73ERYyATbA4GHM9f3dbje8pd2SFa4xF+0Xp09qY380aJrZSWsklBZPUmYiU6+W +i2MlHfF+44rBO9igkUjQKA== +-----END PRIVATE KEY-----)" + +constexpr std::string pub_key_a = + R"(-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvqLsA6WDlgxMjJyYcHoz +p/m6A8cOJ7Yjkd5EcyADbia2xD391CRS/kjz882OzVNQOlz8/zQHHG7353O/HY9d +q7FerLeRrhcfUG8WJ9l0G/L58Y/g4b0qzZPqsuyDGjf9zRqwKFnpkiESFlQe8W1c +m0MrwLnIGkwyp1O9r4jTsaoPL+WxhflrU4ysHs5mLckPGe3aRkGzC9bZExEME6uZ +5y63vBf7en00uM4hlcO0k4IEq6AB2s6Yc2JqnN1uvI2P0kHnzopQC8MxafwsaLm4 +9Hfn9zmE+Y53zD0qSiYmDZS6sQiTxozXEkcY880bf3EWM4QD364jv35GBrUHCzQe +XQIDAQAB +-----END PUBLIC KEY-----)"; +constexpr std::string priv_key_b = + R"(-----BEGIN PRIVATE KEY----- +MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCYq8QNOXZRoAid +R7cKE9byr+9WekPMNDNkaKTjRUoXj8lUgno3y5tIDEIqhcv4thTLAxzQD4N+bVA3 +XF1ZMfm2GmM0O61AtpKwL6diBeGpCTunwzl9nrTeackQmwqRwllc3kW/npudNn12 +M9m4wsgLK98juyY6pZAeTlAvmVkMnFGoyv60jQciWvCFSYkpv2zxAOrmiCjgeYhU ++d8B64qqWmnvdeLl8XGdBYN6nz+vWtWNDi/YuoGI2qhcuiikKvk0Ofmxx3+s4NHS +DqdFfv3CbA5BFBLaHnFHVn+jocEgafOWUjruYcwrUcZuCr8Oy8KLqz6w5Xta/B7x +0Lyx3zvHAgMBAAECggEADQw5ACxWCVnVAqQbZ5gUeb9BhDGE09HuRnmPBgFo+KSI +P1m7WkNjbP/nM70llobxNfx5HOsGgOqUvXZ+X94eikqtCczD3ND9rmMUOhNomsq4 +N3k+05aZvJxr26h0ecqTWpWAfoTupbv/cvexdtHmyNWiB2q6NK7rpztoLPk9HA+q +OzVH/qFbtqr1cQJijyrow97A/Yi2f3Kvp7irlLbH0QxxF9jPW/KDn2FIzycoFUtq +NfuXkUpRkVA82lOyL80uYfQmNkM5/nKJxCTdUtSvA58a2jUC8xVH372kSKikTh6o +clIR8vnvp2aFOrlyz3WfZGZgTo8/MuXP69aujwNgQQKBgQDItvqbcmHjWLIEuheS +ahwIlFFhRR24ytsoRm1HVytBa+tmm56WjPV4chutrEz6IjPd8AvICwpQfCu17iUn +7HM5a0hMctFtVxYuHGnMszD1KpgEByPnv59pPnTbvhqlnRpNR1aM2KVxAXAKSOgY +8u+FA3c4wgUpA3z0l7Db33CUJwKBgQDCuRG8+8+HbQdMmct2+YbId/LSyvnoa9uS +LYXn0WboCOZkEv0KxTjfn2wuLn0WaGG44ucvaFE4hDa7d6cIgrpBLD04rS8xSwa7 +uEQeRrThIn7Gv/RpcTxk0TASIEN2zIi18OV0Wx92wTTv34omFxZLPit9UgiCJM7i +nAFUD6K/YQKBgC33geNRyctIR9S/TaCxfmQUm6KcMpdcld5eaq547yYXchzYrPQr +qhgAggg/Oo3agWhljj0tEhqmpVgQByBijWzr/e3MKdxRonnC9hP0QdUUASaDAB0W +DIsMy7R7kBy3owtpuA+fmhwMST2Bvu3fzSz4QziTbp0a+GYHy3A/dsfnAoGAPYiK +SHQyopMbqWM4XsJ/iz4MZ/xoeMAMxObJ1/XeVRjq5VjyycKFNHWGlBlwwfH+X5Sk +heCrOfbd7OPkztWw0gOO3SgtL6CL4iparE6fvj1OXrQuIlv8P8ezLycu6o277fLQ +L7LUAI0Rk3PKjjrheqmMyK9xrN7A2e9+o/fE8EECgYAx3IziYqFfD4KzgmcM6MKx +t4/SVFXBRLzse8AB3V6qSEwgCaUfeuj0Qq93nrkTIodHFWXuFoQTgQrA29VWbK6x +PSwjdVNwYES+Hg+LbXP8Fo+u5sGhcWLzWdmFp3UdUm5Mv76Oo+MriZNnS4RQiX0+ +Y8PiIt3YYCsowmchtEggaQ== +-----END PRIVATE KEY-----)"; +constexpr std::string pub_key_b = + R"(-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAmKvEDTl2UaAInUe3ChPW +8q/vVnpDzDQzZGik40VKF4/JVIJ6N8ubSAxCKoXL+LYUywMc0A+Dfm1QN1xdWTH5 +thpjNDutQLaSsC+nYgXhqQk7p8M5fZ603mnJEJsKkcJZXN5Fv56bnTZ9djPZuMLI +CyvfI7smOqWQHk5QL5lZDJxRqMr+tI0HIlrwhUmJKb9s8QDq5ogo4HmIVPnfAeuK +qlpp73Xi5fFxnQWDep8/r1rVjQ4v2LqBiNqoXLoopCr5NDn5scd/rODR0g6nRX79 +wmwOQRQS2h5xR1Z/o6HBIGnzllI67mHMK1HGbgq/DsvCi6s+sOV7Wvwe8dC8sd87 +xwIDAQAB +-----END PUBLIC KEY-----)"; + + // Demonstrate some basic assertions. -TEST(AuthorizerTest, BasicAssertions) { - // Expect two strings not to be equal. - EXPECT_STRNE("hello", "world"); - // Expect equality. - EXPECT_EQ(7 * 6, 42); +TEST(TokenTest, TwoWay) { + constexpr std::string issuer = "james-keycloak"; + constexpr std::string username = "james"; + constexpr + const auto token = gen_token( }