Paul's Internet Landfill/ 2022/ Configuring Mutt to work with Duo for uwaterloo.ca Mail

Configuring Mutt to work with Duo for uwaterloo.ca Mail

So the University of Waterloo is steadily but surely moving from internally-hosted Microsoft Exchange to Office365. As an alumnus my number has come up. Soon my UW Mail will be in The Cloud (tm), but I want to continue using Mutt to read my mail. OWA (Outlook Web App, aka webmail) exists and it is okay for searching mail, but it is awful as a daily mail client.

Unfortunately this move means I have to authenticate to the Office 365 using Duo, the UW Multi-factor Authentication system. This looked like a big problem. For a while I tried using Davmail, which allowed me to authenticate to Duo, but was unusably slow. Fortunately, recent versions of Mutt have OAuth2 support, which is what you need to connect to Office365.

tl;dr

Follow the instructions here: https://www.vanormondt.net/~peter/blog/2021-03-16-mutt-office365-mfa.html . As of this writing they work. I will call these the reference instructions. These instructions are worth reading even if you also follow this guide.

Unfortunately for me, I had to work through a bunch of sub-steps in order to get through the above instructions. I will document that yak-shaving below. I will try to be explicit about what information goes where, and in what format, since this is where I tended to get confused.

  1. Configuring Mutt to work with Duo for uwaterloo.ca Mail
    1. tl;dr
    2. Terminology
    3. Overview
    4. Configure GPG
    5. Configure mutt_oauth2.py
    6. Configure IST App Registration
      1. Explanations and Dead Ends
    7. (Maybe) Recompile Mutt
    8. Configure Mutt
    9. Open Questions
  2. Sidebar!

Terminology

There are a bunch of different names to worry about:

My netbook is running Debian Bullseye (11.x). It has no fancy desktop environment.

My laptop is running Ubuntu (actually Xubuntu) Focal (20.04).

There will be a lot of discussion of a client ID and client secret, which are part of an App Registration. I document the Thunderbird client ID and client secret below.

IST is Information Systems and Technology, the IT group that administers email at the university. UW is the University of Waterloo.

Overview

If a mail client supports OAuth2 then (supposedly) it works with Duo. Mutt supports this as of 2018 or so, but the reference instructions say we need Mutt version 2.0.0 or later to support a imap_oauth_refresh_command function in .muttrc .

We also need a Python script called mutt_oauth2.py to do the Oauth2 part. mutt calls this script to do the actual Duo multi-factor auth. We need to hardcode some values into the script (!) and then use it to initialize the OAuth token. Finding these values is an adventure in itself.

Unfortunately, in order to use this Python script we need GPG configured. For me this meant making a new GPG key.

For maximum confusion I will document these steps in (approximately) reverse order.

Configure GPG

tl;dr: Follow the instructions on the GnuPG ArchWiki page.

In particular, I set up a new key using gpg --gen-full-key

I then needed to configure GPG to prompt me for passphrases appropriately. First, in ~/.bashrc I added the line export GPG_TTY=$(tty) . That is all I needed on the netbook.

On the Xubuntu laptop there is some popup that prompts me for a passphrase. I guess I am okay with this but it prompts me too often, and I do not want to store the passphrase in a keyring. Instead I want gpg-agent to cache it for a long time. The internet suggests that I make a file called ~/.gnupg/gpg-agent.conf with the following contents:

no-allow-external-cache
max-cache-ttl 86400
default-cache-ttl 86400

This will make GnuPG (in particular gpg-agent) remember a passphrase for 24 hours. Then to make this take effect I needed to kill (gpgconf --kill gpg-agent) and restart (gpg-agent) the agent.

Configure mutt_oauth2.py

tl;dr: Install the script. Customize the ENCRYPTION_PIPE setting, and then steal the necessary client ID and secret from Thunderbird's source code.

Download the mutt_oauth2.py script from the Mutt Gitlab page here: https://gitlab.com/muttmua/mutt/-/blob/master/contrib/mutt_oauth2.py .

Save the script someplace convenient. I put mine in my ~/bin folder, /home/pnijjar/bin/mutt_oauth2.py . Give the script user execute permissions: chmod u+x ~/bin/mutt_oauth2.py

There are two sections of the script in which we need to hardcode values. The first is to set the ENCRYPTION_PIPE variable with the email address you used when setting up your GPG key. In my case, I changed:

ENCRYPTION_PIPE = ['gpg', '--encrypt', '--recipient', 'YOUR_GPG_IDENTITY']

to

ENCRYPTION_PIPE = ['gpg', '--encrypt', '--recipient', 'pnijjar@example.com']

Next you need to specify the client_id and client_secret fields in the microsoft stanza of the registrations dict. Look for the following code:

    'microsoft': {
        'authorize_endpoint': 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize',
        'devicecode_endpoint': 'https://login.microsoftonline.com/common/oauth2/v2.0/devicecode',
        'token_endpoint': 'https://login.microsoftonline.com/common/oauth2/v2.0/token',
        'redirect_uri': 'https://login.microsoftonline.com/common/oauth2/nativeclient',
        'tenant': 'common',
        'imap_endpoint': 'outlook.office365.com',
        'pop_endpoint': 'outlook.office365.com',
        'smtp_endpoint': 'smtp.office365.com',
        'sasl_method': 'XOAUTH2',
        'scope': ('offline_access https://outlook.office.com/IMAP.AccessAsUser.All '
                  'https://outlook.office.com/POP.AccessAsUser.All '
                  'https://outlook.office.com/SMTP.Send'),
        'client_id': '',
        'client_secret': '',
    },

In principle your Office365 administrator is supposed to generate a client ID and secret for you. IST eventually set me up with access permissions to do this, which I document in the next section.

In practice people just use the values that are hardcoded into the Thunderbird mail client source code. That seems like a terrible idea security-wise but it seems to work. As of this writing, that file contains the following stanza:

    "login.microsoftonline.com",
    [
      "08162f7c-0fd2-4200-a84a-f25a4db0b584", // Application (client) ID
      "TxRBilcHdC6WGBee]fs?QR:SJ8nI[g82", // @see App registrations | Certificates & secrets
      // https://docs.microsoft.com/en-us/azure/active-directory/develop/active-directory-v2-protocols#endpoints
      "https://login.microsoftonline.com/common/oauth2/v2.0/authorize",
      "https://login.microsoftonline.com/common/oauth2/v2.0/token",
    ],

So the client_id and client_secret lines in mutt_oauth2.py become:

        'client_id': '08162f7c-0fd2-4200-a84a-f25a4db0b584',
        'client_secret': 'TxRBilcHdC6WGBee]fs?QR:SJ8nI[g82',

You will want to check the current values in the source code. These secrets do not live longer than 24 months, so depending on when you read this the actual client_secret is likely different.

Now we can initialize the OAuth2 token by calling the script: ~/bin/mutt_oauth2.py ~/.mutt_token --verbose --authorize

You should be asked for a registration (microsoft), a preferred OAuth2 flow (localhostauthcode), and an account e-mail address (your UW address, so pntestin@uwaterloo.ca for me).

If you are lucky you will see a giant URL you are supposed to copy and paste into a web browser. Paste that URL into your web browser and authenticate to UW using the Duo system. If you are lucky you will get a message like NOTICE: Obtained new access token followed by a large amount of random text representing the token you received.

If you are unlucky you will get an error message. I was getting AADSTS7000215: Invalid client secret provided. but that is because I had pasted the client_secret incorrectly and did not notice for hours.

Configure IST App Registration

tl;dr: The mutt_oauth2.py README is mostly correct, but as of this writing the platform is wrong.

I started out using the Thunderbird Client ID and secret, but I also asked the IST helpdesk whether we had something internal to UW. To my surprise, IST gave me access to the Microsoft Azure App Registration portal, and they (partially) set up a Client ID specifically for me. I needed to configure a Client Secret, and configure a bunch of permissions and settings. The above README had most of the information I needed, but (as of this writing) it appeared to have one error.

To complete this step it was very helpful to have two different web browsers running. I logged into Azure using Firefox, and ran the authentication using an incognito Chromium window. This way I could close the Chromium window and log in fresh again.

IST sent me an email telling me that I had been given access to the App Registration component: "PIM: You now have the Application Registration Owners role". Then I started configuring the app registration.

I then changed the following settings:

After saving this, I was able to use the new Client ID and Client Secret in mutt_oauth2.py and authorize. (I deleted the old .mutt_token file and started fresh each time I retried this.)

Explanations and Dead Ends

Why use the "Web" platform and not the "Mobile and desktop" one? When I tried the latter I got the error message AADSTS700025: Client is public so neither 'client_assertion' nor 'client_secret' should be presented. After a bunch of reading I learned that Microsoft classifies some apps as "confidential" and others as "public". "Web" apps are confidential (!) and "Mobile and desktop" ones are public. Public apps are more restricted in that they cannot pass around secrets, and client_secret is a secret.

Why specify a platform at all? I tried not doing this and received the error message AADSTS500113: No reply address is registered for the application.

Why specify the "Redirect URI" to localhost? It turns out that there is a redirect_uri parameter encoded in the giant URL you paste into Chromium. That was something like http://localhost:53841 where 53841 is a port that is randomly-generated by the script. I had other values there before; when I did so I got the error message AADSTS50011: The redirect URI 'http://localhost:53841/' specified in the request does not match the redirect URIs configured for the application I guess we got lucky that the Thunderbird app registration also uses localhost?

Why specify a multi-tenant application as opposed to a single tenant one? There are scary warnings ("Starting November 9th, 2020 end users will no longer be able to grant consent to newly registered multitenant apps without verified publishers") that show up if you specify multi-tenant. But when I specified single tenant I got the error message AADSTS50194: Application '...'(pnijjar - mutt email application) is not configured as a multi-tenant application. Usage of the /common endpoint is not supported for such applications created after '10/15/2018'. Use a tenant-specific endpoint or configure the application to be multi-tenant. I could not figure out what the tenant-specific endpoint was supposed to be. In the mutt_oauth2.py script we would need to change the tenant parameter to match this in addition to changing client_id and client_secret.

(Maybe) Recompile Mutt

tl;dr: Ignore this unless you are running a version of Ubuntu/Debian that has Mutt 1.x . Otherwise follow instructions from this defunct blog post.

The version of Mutt on Debian Bullseye was good enough that I could use it directly. The version of Mutt on Ubuntu Focal was too old. I ended up recompiling this, but it was not too difficult. My use case was simple: I just wanted a newer version, and there is one available in Ubuntu Impish.

First, add or change the deb-src line in /etc/apt/sources.list to point to Impish:

deb-src http://ca.archive.ubuntu.com/ubuntu impish main restricted

Next, run some commands to update your sources, install dependencies and make the .deb file. I made a folder called src for this. The commands that require root are prefixed with sudo:

mkdir src
cd src
sudo apt update
sudo apt-get build-dep mutt
apt-get -b source mutt

This downloaded the 2.0.5 version of Mutt and compiled it for me, resulting in a mutt_2.0.5-4.1build1_amd64.deb file. I could then install it as root:

sudo dpkg -i mutt_2.0.5-4.1build1_amd64.deb

I had installed other things like build-essential and devscripts earlier, so maybe you need to do that too? I am actively surprised that this compilation works, because the version of Mutt in Impish depends on something called libidn2 which does not exist in Ubuntu Focal at all. (There is an older library called libidn11 there.) Apparently this library is for internationalized domain names, which is no big deal for me but might hurt you.

Configure Mutt

tl;dr: None, really. You need this configuration to get mail working.

Finally you need to modify your .muttrc or equivalent. The relevant lines for my account were:

# Where should my inbox be?
set folder="imaps://pntestin@uwaterloo.ca@outlook.office365.com"
set imap_keepalive=15
set timeout=20

set imap_user="pntestin@uwaterloo.ca"
set spoolfile=+Inbox
set imap_authenticators="xoauth2"
set imap_oauth_refresh_command="/home/pnijjar/bin/mutt_oauth2.py /home/pnijjar/.mutt_token"
set smtp_authenticators = ${imap_authenticators}
set smtp_oauth_refresh_command = ${imap_oauth_refresh_command}

set smtp_url="smtp://pntestin@uwaterloo.ca@smtp.office365.com:587"
set ssl_starttls=yes

set from="pntestin@uwaterloo.ca"
set realname="Paul Nijjar (UW Testing)"
set hostname="uwaterloo.ca"

Note the weird format for the set folder and set smtp_url lines. They have two @ symbols each.

Note the imap_authenticators is set to xoauth2.

Note the imap_oauth_refresh_command points to the Python script and the token we authorized before.

I have other .muttrc settings (and you probably do too) but I think these are the ones I needed to get Mutt working with Duo.

Open Questions

I am not sure how often I need to reauthorize the token. Duo has an option to remember a token for 30 days but I don't want that. I also do not want to re-authenticate too frequently.

I do not know whether the Thunderbird client ID and client secret will work in the medium term. I do not know whether I will actually be able to generate a secret again in two years if I have been away from UW for several years.

I do not know what endpoint to use if I have a single-tenant application and not a multi-tenant one.

I do not know the correct setting for "Implicit Grant and Hybrid Flows" in the App Registration.

Obviously if there are security bugs in the version of Mutt packaged for Impish I will not get them unless I recompile and reinstall. I think that is unavoidable, but I am not sure.