Sabits’ Space

OpenID Connect and ZNC, Part 1

ZNC has two main modules for sign-in, cyrusauth and imapauth In evaluating them, I found some major shortcomings that wouldn’t be easy to correct; imapauth requires the user to exist in ZNC’s configuration before they can log in, and both cyrusauth, imapauth and the core of ZNC don’t allow users to change their username. Additionally, imapauth would require me to either run another dovecot instance or have scope validation not work properly and cyrusauth would require me to write a saslauthd.

Writing a ZNC plugin is fairly easily, and using cyrusauth as an example to learn what functions we need to override to implement an authentication provider we get the following:

 1 2 3 4 5 6 7 8 910111213141516171819202122232425
#include <znc/znc.h>

class COpenIDCAuth : public CModule {
    public:
        MODCONSTRUCTOR(COpenIDCAuth) {
        }

        ~COpenIDCAuth() override {}

        bool OnLoad(const CString& args, CString& message) override {
            return true;
        }

        EModRet OnLoginAttempt(std::shared_ptr<CAuthBase> Auth) override {
            CUser *user = nullptr;
            if (!checkLogin(Auth->GetUsername(), Auth->GetPassword())) {
                Auth->RefuseLogin("Invalid username or password");
                return HALT;
            }
            Auth->AcceptLogin(*user);
            return HALT;
        }
};

GLOBALMODULEDEFS(COpenIDCAuth, "OpenID Connect Auth Method")

Now ZNC doesn’t have any built-in library for making http requests, so we’ll use libcurl via the wonderful Curl for People. OAuth2 and by extension OpenID Connect both make heavy use of JSON so we’ll also make use of nlohmann’s JSON for Modern C++.

We are going to use the OAuth 2 Resource Owner Password Credentials Grant as defined in section 4.3 of RFC 6749 to provide authentication for ZNC. To get around ZNC’s inability to change usernames we’ll be creating users using the subject-identifier as their username.

Since I hadn’t implemented user cloning, I manually created the user in the znc.conf and started znc up, only to have it terminate with an error:

[ ** ] Invalid user [21ffb2d9-18f0-40b0-8d6a-7c166c52fa56] Username is invalid
[ ** ] Unrecoverable config error.

Apparently, “21ffb2d9-18f0-40b0-8d6a-7c166c52fa56” is an invalid username. So I grep through the ZNC source and discover CUser::IsValidUserName gets called when parsing the configuration.

 1 2 3 4 5 6 7 8 91011121314151617181920212223
bool CUser::IsValidUserName(const CString& sUserName) {
    // /^[a-zA-Z][a-zA-Z@._\-]*$/
    const char* p = sUserName.c_str();


    if (sUserName.empty()) {
        return false;
    }

    if ((*p < 'a' || *p > 'z') && (*p < 'A' || *p > 'Z')) {
        return false;
    }

    while (*p) {
        if (*p != '@' && *p != '.' && *p != '-' && *p != '_' && !isalnum(*p)) {
            return false;
        }

        p++;
    }

    return true;
}

The function has a handy comment at the top which gives the validation in regular expression form. We can see that since the username doesn’t start with a letter it is therefore invalid.

Now the SSO provider I’m using uses UUIDs for subject identifiers so if I were to just prefix the username with a string starting with a letter, the rest of the username would pass validation because UUIDs only contain hexadecimal and dash separators. However, OpenID Connect makes no such guarantees about the subject identifier, implementations can use any unique string for it.

We can implement something similar to percent-encoding except using an “@” instead of a percent sign for escaping characters that would otherwise be invalid in an username. This in addition to prepending some letters should be enough to satisfy the requirements of a username in ZNC.

 1 2 3 4 5 6 7 8 91011121314151617181920
const CString
EscapeUserName(const CString& UserName)
{
    const char hex[] = "0123456789ABCDEF";

    CString ret = "openidc-";
    ret.reserve(UserName.length()+8);

    for (const char& c : UserName) {
        if (c != '.' && c != '-' && c != '_' && !isalnum(c)) {
            ret += '@';
            ret += hex[c >> 4];
            ret += hex[c & 0xF];
        } else {
            ret += c;
        }
    }

    return ret;
}