Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
.idea
/cmake-build-debug
/build
/packaging-build
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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/<YOUR TENANT ID>/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
Expand Down
14 changes: 13 additions & 1 deletion docs/sasl-xoauth2.conf.5.md
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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`

Expand All @@ -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.
Expand Down
11 changes: 11 additions & 0 deletions src/config.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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) {
Expand Down
6 changes: 6 additions & 0 deletions src/config.h
Original file line number Diff line number Diff line change
Expand Up @@ -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_; }
Expand All @@ -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_ = "";
Expand Down
59 changes: 51 additions & 8 deletions src/token_store.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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<std::string> *output) {
if (root.isMember(key)) {
Expand All @@ -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());

Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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_);
Expand All @@ -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) {
Expand All @@ -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);
Expand All @@ -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()) {
Expand Down
5 changes: 4 additions & 1 deletion src/token_store.h
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,13 @@ class TokenStore {
std::optional<std::string> override_proxy_;
std::optional<std::string> override_ca_bundle_file_;
std::optional<std::string> override_ca_certs_dir_;
std::optional<std::string> override_scope_;
std::optional<int> override_refresh_window_ = 0;
std::optional<bool> manage_token_externally_ = false;
std::optional<bool> use_client_credentials_ = false;

std::string access_;
std::string refresh_;
std::optional<std::string> refresh_;
std::optional<std::string> user_;
time_t expiry_ = 0;

Expand Down