diff --git a/.gitignore b/.gitignore index 302f652..832150f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ +.idea +/cmake-build-debug /build /packaging-build diff --git a/README.md b/README.md index 04a81f4..7b22e51 100644 --- a/README.md +++ b/README.md @@ -201,6 +201,8 @@ sasl-xoauth2 also provides two configuration variables, `ca_bundle_file` and libraries will look for a CA certificate bundle (for `ca_bundle_file`) or a set of CA certificates (for `ca_certs_dir`). Specify one or the other, but not both. +**Remember curl expect a new line after the last certificate in the bundle. If it is missed curl aborts with an read error.** + #### A Note on postmulti [@jamenlang](https://github.com/jamenlang) has provided a [very helpful @@ -403,6 +405,25 @@ or: $ sudo chown -R postfix:postfix /var/spool/postfix/etc/tokens ``` +### Outlook/Office 365 Configuration (Client Credentials Flow) + +The initial work at azure entra platform for creating an app is the same like the device flow. +For preventing initial interaction with a browser we add a secret to the app and can use it for the client credential flow. + +Store the client ID and secret in `/etc/sasl-xoauth2.conf`: + +```json +{ + "client_id": "client ID goes here", + "client_secret": "client secret goes here", + "use_client_credentials": "true", + "scope": "https://outlook.office365.com/.default", + "token_endpoint": "https://login.microsoftonline.com//oauth2/v2.0/token" +} +``` + +This flow prevents the need of a refresh token in the token answer. + ### Outlook/Office 365 Configuration (Legacy Client) (Deprecated) #### Client Credentials diff --git a/docs/sasl-xoauth2.conf.5.md b/docs/sasl-xoauth2.conf.5.md index a398f05..c701938 100644 --- a/docs/sasl-xoauth2.conf.5.md +++ b/docs/sasl-xoauth2.conf.5.md @@ -34,6 +34,10 @@ The top-level JSON object can contain the following keys: : authenticates this client for OAuth 2 token requests; world-readable by default (but see below to place this in token files instead) +`scope` + +: if the client credentials grant is used, we need a scope. Specially for office365.com where the default scope should be: `https://outlook.office365.com/.default` + `always_log_to_syslog` : always write plugin log messages to syslog, even for successful runs; may contain tokens/secrets (defaults to "no") @@ -56,7 +60,7 @@ The top-level JSON object can contain the following keys: `ca_bundle_file` -: if set, overrides CURL's default certificate-authority bundle file +: if set, overrides CURL's default certificate-authority bundle file. **Remember curl expect a new line after the last certificate in the bundle**. `ca_certs_dir` @@ -66,6 +70,14 @@ The top-level JSON object can contain the following keys: : if set, overrides the default 10 second refresh window with the specified time in seconds (integer) +`manage_token_externally` + +: if set, especially in the token file, the plugin takes the token as is + +`use_client_credentials` + +: if set, the client credentials grant flow is used, instead of the refresh_token flow + # TOKEN FILE In addition to this file, `sasl-xoauth2` relies on a "token file" which it updates independently. diff --git a/src/config.cc b/src/config.cc index b3a93f6..4e8cceb 100644 --- a/src/config.cc +++ b/src/config.cc @@ -151,6 +151,9 @@ int Config::Init(const Json::Value &root) { err = Fetch(root, "client_secret", false, &client_secret_); if (err != SASL_OK) return err; + err = Fetch(root, "scope", true, &scope_); + if (err != SASL_OK) return err; + err = Fetch(root, "always_log_to_syslog", true, &always_log_to_syslog_); if (err != SASL_OK) return err; @@ -178,6 +181,14 @@ int Config::Init(const Json::Value &root) { err = Fetch(root, "ca_certs_dir", true, &ca_certs_dir_); if (err != SASL_OK) return err; + err = Fetch(root, "manage_token_externally", true, + &manage_token_externally_); + if (err != SASL_OK) return err; + + err = Fetch(root, "use_client_credentials", true, + &use_client_credentials_); + if (err != SASL_OK) return err; + return 0; } catch (const std::exception &e) { diff --git a/src/config.h b/src/config.h index c52d311..b995b94 100644 --- a/src/config.h +++ b/src/config.h @@ -34,9 +34,12 @@ class Config { std::string client_id() const { return client_id_; } std::string client_secret() const { return client_secret_; } + std::string scope() const { return scope_; } bool always_log_to_syslog() const { return always_log_to_syslog_; } bool log_to_syslog_on_failure() const { return log_to_syslog_on_failure_; } bool log_full_trace_on_failure() const { return log_full_trace_on_failure_; } + bool manage_token_externally() const { return manage_token_externally_; } + bool use_client_credentials() const { return use_client_credentials_; } std::string token_endpoint() const { return token_endpoint_; } std::string proxy() const { return proxy_; } std::string ca_bundle_file() const { return ca_bundle_file_; } @@ -50,9 +53,12 @@ class Config { std::string client_id_; std::string client_secret_; + std::string scope_; bool always_log_to_syslog_ = false; bool log_to_syslog_on_failure_ = true; bool log_full_trace_on_failure_ = false; + bool manage_token_externally_ = false; + bool use_client_credentials_ = false; std::string token_endpoint_ = "https://accounts.google.com/o/oauth2/token"; std::string proxy_ = ""; std::string ca_bundle_file_ = ""; diff --git a/src/token_store.cc b/src/token_store.cc index c778d50..0f176de 100644 --- a/src/token_store.cc +++ b/src/token_store.cc @@ -48,6 +48,14 @@ std::string GetTempSuffix() { return std::string(buf); } +bool s_to_b(const std::string in) { + std::for_each(in.begin(), in.end(), [](char c) { return std::tolower(c); }); + if (in == "yes" || in == "true") { + return true; + } + return false; +} + void ReadOverride(const Json::Value &root, const std::string &key, std::optional *output) { if (root.isMember(key)) { @@ -73,6 +81,14 @@ void WriteOverride(const std::string &key, } int TokenStore::GetAccessToken(std::string *token) { + const bool manage_token_externally = manage_token_externally_.value_or( + Config::Get()->manage_token_externally()); + + if ( manage_token_externally ) { + *token = access_; + return SASL_OK; + } + const int refresh_window = override_refresh_window_.value_or(Config::Get()->refresh_window()); @@ -109,10 +125,15 @@ int TokenStore::Refresh() { const std::string ca_certs_dir = override_ca_certs_dir_.value_or(Config::Get()->ca_certs_dir()); + const bool use_client_credentials = use_client_credentials_.value_or(Config::Get()->use_client_credentials()); + const std::string request = std::string("client_id=") + client_id + "&client_secret=" + client_secret + - "&grant_type=refresh_token&refresh_token=" + refresh_; + (use_client_credentials + ? (std::string("&grant_type=client_credentials&scope=") + override_scope_.value_or(Config::Get()->scope())) + : ("&grant_type=refresh_token&refresh_token=" + refresh_.value_or(""))); + std::string response; long response_code = 0; log_->Write("TokenStore::Refresh: token_endpoint: %s", @@ -188,9 +209,24 @@ int TokenStore::Read() { Json::Value root; file >> root; - if (!root.isMember("refresh_token")) { - log_->Write("TokenStore::Read: missing refresh_token"); - return SASL_FAIL; + + if (root.isMember("manage_token_externally")) + manage_token_externally_ = + s_to_b(root["manage_token_externally"].asString()); + if (root.isMember("use_client_credentials")) + use_client_credentials_ = s_to_b(root["use_client_credentials"].asString()); + + const bool manage_token_externally = manage_token_externally_.value_or( + Config::Get()->manage_token_externally()); + const bool use_client_credentials = use_client_credentials_.value_or( + Config::Get()->use_client_credentials()); + + if (!(manage_token_externally || use_client_credentials)) { + if (!root.isMember("refresh_token")) { + log_->Write("TokenStore::Read: missing refresh_token"); + return SASL_FAIL; + } + refresh_ = root["refresh_token"].asString(); } ReadOverride(root, "client_id", &override_client_id_); @@ -199,19 +235,19 @@ int TokenStore::Read() { ReadOverride(root, "proxy", &override_proxy_); ReadOverride(root, "ca_bundle_file", &override_ca_bundle_file_); ReadOverride(root, "ca_certs_dir", &override_ca_certs_dir_); + ReadOverride(root, "scope", &override_scope_); if (root.isMember("refresh_window")) override_refresh_window_ = stoi(root["refresh_window"].asString()); - refresh_ = root["refresh_token"].asString(); if (root.isMember("access_token")) access_ = root["access_token"].asString(); if (root.isMember("expiry")) expiry_ = stoi(root["expiry"].asString()); ReadOverride(root, "user", &user_); - log_->Write("TokenStore::Read: refresh=%s, access=%s, user=%s", - refresh_.c_str(), access_.c_str(), user_.value_or("").c_str()); + log_->Write("TokenStore::Read: access=%s, user=%s", access_.c_str(), + user_.value_or("").c_str()); return SASL_OK; } catch (const std::exception &e) { @@ -230,11 +266,12 @@ int TokenStore::Write() { try { Json::Value root; - root["refresh_token"] = refresh_; root["access_token"] = access_; root["expiry"] = std::to_string(expiry_); + WriteOverride("refresh_token", refresh_, &root) ; WriteOverride("user", user_, &root); + WriteOverride("scope", override_scope_, &root); WriteOverride("client_id", override_client_id_, &root); WriteOverride("client_secret", override_client_secret_, &root); @@ -246,6 +283,12 @@ int TokenStore::Write() { if (override_refresh_window_) { root["refresh_window"] = std::to_string(*override_refresh_window_); } + if (use_client_credentials_.has_value()) { + root["use_client_credentials"] = use_client_credentials_.value_or(false) ? "true" : "false"; + } + if (manage_token_externally_.has_value()) { + root["manage_token_externally"] = manage_token_externally_.value_or(false) ? "true" : "false"; + } std::ofstream file(new_path); if (!file.good()) { diff --git a/src/token_store.h b/src/token_store.h index c290bba..4990762 100644 --- a/src/token_store.h +++ b/src/token_store.h @@ -55,10 +55,13 @@ class TokenStore { std::optional override_proxy_; std::optional override_ca_bundle_file_; std::optional override_ca_certs_dir_; + std::optional override_scope_; std::optional override_refresh_window_ = 0; + std::optional manage_token_externally_ = false; + std::optional use_client_credentials_ = false; std::string access_; - std::string refresh_; + std::optional refresh_; std::optional user_; time_t expiry_ = 0;