-
Notifications
You must be signed in to change notification settings - Fork 0
Home
simple-sasl-xoauth2 is a very simple Cyrus SASL client plugin. Overall, the SASL authentication process is surprisingly simple. An application creates a SASL client, the application registers SASL callbacks to provide a username and password, the SASL XOAUTH2 client plugin initialises, and then the plugin requests the username and password from the application. In the case of XOAUTH2, the password is actually an OAuth2 token, but it's the same overall principle.
The nice thing about this use case is that it can be fully implemented locally using open source software. This means that you can test out this plugin and your knowledge of SASL authentication, SMTP, and OAuth2 without needing to create an account with a third-party.
An easy way to set this up is to create the following docker-compose.yml file.
networks:
xoauth2-net:
services:
dev:
command: '/bin/bash -c "trap : TERM INT; sleep infinity & wait"'
image: ubuntu:24.04
privileged: true
volumes:
- .:/root/simple-sasl-xoauth2
networks:
- xoauth2-net
keycloak:
image: quay.io/keycloak/keycloak:26.6
environment:
KC_BOOTSTRAP_ADMIN_USERNAME: keycloak
KC_BOOTSTRAP_ADMIN_PASSWORD: keycloak
command: start-dev
ports:
- "8080:8080"
networks:
- xoauth2-net
dovecot:
image: dovecot/dovecot:latest
volumes:
- ./files/dovecot/config:/etc/dovecot/conf.d:ro
networks:
- xoauth2-net
security_opt:
- no-new-privileges:true
depends_on:
- keycloak
In "./files/dovecot/config" you'll want to create two files:
service submission-login {
chroot =
}
service imap-login {
chroot =
}
service pop3-login {
chroot =
}
service managesieve-login {
chroot =
}
service imap-urlauth-login {
chroot =
}
auth_mechanisms {
oauthbearer = yes
xoauth2 = yes
plain = no
login = no
}
ssl = no
auth_allow_cleartext = yes
submission_relay_host = localhost
oauth2 {
introspection_url = http://keycloak:8080/realms/test/protocol/openid-connect/userinfo
introspection_mode = auth
username_attribute = preferred_username
}
Note that this is NOT a production configuration. This is only for TESTING. In production, you'll want to use SSL/TLS and a real relay host.
In your OS's hosts file, you'll want to map the hostname "keycloak" to 127.0.0.1. This will be important.
Next, you'll want to go to http://keycloak:8080/ and login with the credentials specified in the docker-compose.yml.
We click on "Manage realms", click "Create realm", fill in "Realm name" with the value "test", and click "Create".
Next, we'll click on "Clients", and click "Create client". For "Client id", we're going to use the value "dovecot" then click "Next". We'll click the slider for "Client authentication", and we'll check the checkbox for "Service account roles", and then click "Next", and then we'll click "Save". Next, we'll click "Credentials" and we'll click the eye or "Show password" icon to get the "Client Secret".
Now to write a quick Perl script. Luckily, I've already prepared one. But first, if we're on Ubuntu 24.04, we'll want to install some dependencies first.
docker exec -it simple-sasl-xoauth2-dev-1 bash
cd /root/simple-sasl-xoauth2
apt-get update
apt install gcc make libsasl2-dev
apt install libauthen-sasl-xs-perl libwww-perl libjson-perl
make clean
make
make install
Then we'll create test_email.pl:
#!/usr/bin/perl
use strict;
use warnings;
use Net::SMTP;
use Authen::SASL;
use Getopt::Long;
use LWP::UserAgent;
use JSON;
my $recipient;
my $client_id;
my $client_password;
my $username;
GetOptions(
'to=s' => \$recipient,
'client_id=s' => \$client_id,
'client_secret=s' => \$client_password,
'username=s' => \$username,
) or die("Error in command line arguments\n");
if ( !defined $recipient || !defined $client_id || !defined $client_password || !defined $username ) {
die "Usage: $0 --to <value> --client_id <id> --client_password <pass> --username <user>\n";
}
unless (defined($recipient) && ($recipient =~ /^[A-Z0-9_+]+([A-Z0-9_+.)*@(?:[A-Z0-9-]+\.)+[A-Z]{2,6}$/i)) {
print STDERR "Usage: $0 email\@domain.tld\n";
exit 1;
}
my $ua = LWP::UserAgent->new;
$ua->timeout(10);
my $response = $ua->post(
"http://keycloak:8080/realms/test/protocol/openid-connect/token",
Content => {
grant_type => "client_credentials",
client_id => $client_id,
client_secret => $client_password,
scope => "openid profile",
}
);
my $password = "";
if ($response->is_success){
my $data = decode_json($response->decoded_content);
if ($data && $data->{access_token}){
$password = $data->{access_token};
}
}
else {
die $response->status_line;
}
my $smtp = Net::SMTP->new(
'dovecot',
Port => 31587,
Timeout => 30,
Debug => 1,
);
die "Could not connect to server: $!" unless $smtp;
#$smtp->starttls(SSL_verify_mode => 0)
# or confess "Failed to start TLS: ".$!;
my $sasl = Authen::SASL->new(
mechanism => 'XOAUTH2',
callback => { auth => $username, password => $password }
);
#500 5.5.2 Line too long
#NOTE: If the SMTP response is over 1000 characters, Dovecot will reject it
$smtp->auth($sasl)
or die "Failed to authenticate: $!";
=pod
# Send email
$smtp->mail($username);
$smtp->to($recipient);
$smtp->data();
$smtp->datasend("To: $recipient\n");
$smtp->datasend("From: $username\n");
$smtp->datasend("Subject: Test Email\n");
$smtp->datasend("\n");
$smtp->datasend("This is a test email.\n");
$smtp->dataend();
=cut
$smtp->quit;
To run test_email.pl, we do the following:
perl test_email.pl --to test@test.test --username service-account-dovecot --client_id dovecot --client_secret "client_secret_from_keycloak"
At this point, we're probably going to see something like this: Net::SMTP=GLOB(0x5635f1adbea8)<<< 500 5.5.2 Line too long
This is because Net::SMTP isn't very good at handling SASL passwords longer than 1000 characters. Since the "AUTH XOAUTH2 " is too long, Dovecot is rejecting it. So we'll reduce the size of the token...
In Keycloak, we go to the "dovecot" client, click on the "Service account roles", check the box next to "default-roles-test", and click "Unassign" and click the "Remove" button. That's a bit shorter, but still not good enough.
Next we'll click "Realm settings", click the "Tokens" tab, and change "Default Signature Algorithm" from RS256 to ES256 which creates a shorter signature for the JWT in the access token. We click "Save" to finalise this change. Unfortunately, it's still not enough, so we keep trying.
We go to the "dovecot" client, click on "Client scopes", and change all the "Default" scopes to "Optional".
Now we try again...
perl test_email.pl --to test@test.test --username service-account-dovecot --client_
id dovecot --client_secret IHEuNs2sPLwYot8rSJzItzz6L03ICxBn
Net::SMTP>>> Net::SMTP(3.15)
Net::SMTP>>> Net::Cmd(3.15)
Net::SMTP>>> Exporter(5.77)
Net::SMTP>>> IO::Socket::IP(0.4101)
Net::SMTP>>> IO::Socket(1.52)
Net::SMTP>>> IO::Handle(1.52)
Net::SMTP=GLOB(0x5646d9d05338)<<< 220 166fad8d44c3 Dovecot ready.
Net::SMTP=GLOB(0x5646d9d05338)>>> EHLO localhost.localdomain
Net::SMTP=GLOB(0x5646d9d05338)<<< 250-166fad8d44c3
Net::SMTP=GLOB(0x5646d9d05338)<<< 250-8BITMIME
Net::SMTP=GLOB(0x5646d9d05338)<<< 250-AUTH OAUTHBEARER XOAUTH2
Net::SMTP=GLOB(0x5646d9d05338)<<< 250-SMTPUTF8
Net::SMTP=GLOB(0x5646d9d05338)<<< 250-BURL imap
Net::SMTP=GLOB(0x5646d9d05338)<<< 250-CHUNKING
Net::SMTP=GLOB(0x5646d9d05338)<<< 250-ENHANCEDSTATUSCODES
Net::SMTP=GLOB(0x5646d9d05338)<<< 250-SIZE
Net::SMTP=GLOB(0x5646d9d05338)<<< 250 PIPELINING
Net::SMTP=GLOB(0x5646d9d05338)>>> AUTH XOAUTH2 dXNlcj1zZXJ2aWNlLWFjY291bnQtZG92ZWNvdAFhdXRoPUJlYXJlciBleUpoYkdjaU9pSkZVekkxTmlJc0luUjVjQ0lnT2lBaVNsZFVJaXdpYTJsa0lpQTZJQ0pqVjNsSVVEbHhialJvYlRKR016aFVNWEJYVkRGMGJqZHdkVVJtUWxGWk9IUnFUV2t0ZVhaV1pURm5JbjAuZXlKbGVIQWlPakUzT0RFMU16QXlNakFzSW1saGRDSTZNVGM0TVRVeU9Ua3lNQ3dpYW5ScElqb2lkSEp5ZEdOak9tTXdZalJpTXpRMExUZzNOemt0TVRCbU1pMHpaVEk1TFRSbU5XVmtNekV3TldKak1DSXNJbWx6Y3lJNkltaDBkSEE2THk5clpYbGpiRzloYXpvNE1EZ3dMM0psWVd4dGN5OTBaWE4wSWl3aWMzVmlJam9pWWpCbU5XRTBOR1F0WldFNFpTMDBNREZoTFRnek9HVXRabVl6TWpjeE56Y3dNalU0SWl3aWRIbHdJam9pUW1WaGNtVnlJaXdpWVhwd0lqb2laRzkyWldOdmRDSXNJbUZzYkc5M1pXUXRiM0pwWjJsdWN5STZXeUlpWFN3aWMyTnZjR1VpT2lKdmNHVnVhV1FnY0hKdlptbHNaU0lzSW1Oc2FXVnVkRWh2YzNRaU9pSXhOekl1TVRndU1DNHpJaXdpY0hKbFptVnljbVZrWDNWelpYSnVZVzFsSWpvaWMyVnlkbWxqWlMxaFkyTnZkVzUwTFdSdmRtVmpiM1FpTENKamJHbGxiblJCWkdSeVpYTnpJam9pTVRjeUxqRTRMakF1TXlJc0ltTnNhV1Z1ZEY5cFpDSTZJbVJ2ZG1WamIzUWlmUS43eFNvSkk5OUQ0QnJPS09FSFEtcnZHdWFjeXlKS2dHMzcyTlN3M0MtS3pWdkRYWm5GZW1pZE80VWtCMXA4MllkczBRbzRENjg1YXFNMmVFR2I2dWtSUQEB
Net::SMTP=GLOB(0x5646d9d05338)<<< 235 2.7.0 Logged in.
Net::SMTP=GLOB(0x5646d9d05338)>>> QUIT
Net::SMTP=GLOB(0x5646d9d05338)<<< 421 4.4.0 166fad8d44c3 Failed to connect to relay server (connect)
Success! We can see "235 2.7.0 Logged in" which shows that our XOAUTH2 authentication was successful.
Just one last reminder that this is not a production setup! This example is provided just for showing how the simple-sasl-xoauth2 plugin can be used.
If you want to test out Postfix as a SMTP client using Microsoft as a relayhost, try the following:
docker exec -it simple-sasl-xoauth2-dev-1 bash
apt-get update
apt-get install postfix
postconf maillog_file=/var/log/postfix.log
vi /etc/postfix/main.cf
Then in main.cf:
#Update relayhost to the following:
relayhost = [smtp.office365.com]:587
#Update inet_protocols to the following:
inet_protocols = ipv4
#Add the following to the bottom of the configuration file:
smtp_sender_dependent_authentication = yes
sender_dependent_relayhost_maps = hash:/etc/postfix/sender_relay
smtp_sasl_auth_enable = yes
smtp_sasl_password_maps = hash:/etc/postfix/sasl_passwd
smtp_sasl_security_options =
smtp_sasl_mechanism_filter = xoauth2
You'll also need to update /etc/postfix/sender_relay:
user@domain.tld [smtp.office365.com]:587
And you'll want to update /etc/postfix/sasl_passwd:
user@domain.tld user@domain.tld:<TOKEN>
After updating these two files, you'll need to run the following:
postmap /etc/postfix/sender_relay
postmap /etc/postfix/sasl_passwd
service postfix restart
Now you should be able to use whatever mail tool you want to send emails to Postfix and it will send them via Microsoft's Office 365 SMTP server.
Note that this is not an optimal example because you're having to hard-code the OAuth2 token into /etc/postfix/sasl_passwd.
The config smtp_sasl_password_maps can take quite a few different map/lookup options. If you look at Postfix lookup table types you can see all of them. The most dynamic option is going to be "socketmap" using a Unix socket with carefully assigned file permissions.
As a separate project, the author of this SASL plugin is working on a token service which will present a Unix socket for Postfix to query.