PostfixからXOAUTH2認証を利用してAzure ADアカウントでメール送信する

# はじめに
[神サイト](https://github.com/tarickb/sasl-xoauth2)のStarを1万回押すのに代えて本稿を記す。

### 転送ルール
条件: メール送信者が特定のAzure AD(Microsoft Entra ID)アカウントである場合
転送先: smtp.office365.com
(それ以外は受信者のMXホストへ直接送信する)

タイトルに「メール送信」とあるが、Postfixの動作としてはリレー(中継先への転送)にあたるため「転送」と記す場合あり。


### 環境
Amazon Linux 2 / RHEL 8 互換OS (AlmaLinux 8)

### 手順概要
1. Azure ADアプリケーションの登録
2. Postfix, Cyrus SASLインストール
3. [Amazon Linux 2] sasl-xoauth2のビルド、設定
4. アクセストークン取得、SMTP認証確認
5. Postfix設定、メール送信確認


# 1. Azure ADアプリケーションの登録

#### 1-1. アプリケーションの登録
メール送信者であるAzure ADアカウントで[Azure](https://portal.azure.com/)にログイン
Azure Active Directory > アプリの登録 > 新規登録
サポートされているアカウントの種類: この組織ディレクトリのみに含まれるアカウント (既定のディレクトリ のみ - シングル テナント)
リダイレクトURL: (設定不要)
登録(保存)し、アプリの概要で **"アプリケーション (クライアント) ID"** と **"ディレクトリ (テナント) ID"** を確認しメモする。

#### 1-2. アプリ設定1(認証)
アプリ > [認証]
パブリック クライアント フローを許可する - 次のモバイルとデスクトップのフローを有効にする: 「はい」を選択

#### 1-3. アプリ設定2(APIのアクセス許可)
アプリ > [APIのアクセス許可] > アクセス許可の追加
Microsoft Graph > 委任されたアクセス許可
SMTP.Send を追加して保存
[Azure AD管理者] 同じ画面を開き「管理者の同意の付与」を実行

#### 1-4. アプリ設定3(クライアントシークレットの作成)
通常不要だが、「サポートされているアカウントの種類」の設定値によっては[必要になる場合あり](https://github.com/tarickb/sasl-xoauth2#a-note-on-client-secrets)。(動作未確認)

#### 1-5. [Azure AD管理者] アプリのサービス プリンシパルをExchangeに登録
Azure Active Directory > エンタープライズアプリケーション > アプリで **"オブジェクト ID"** をメモする(「アプリの登録」の[概要]にも同じものがあるがそれではない)。
PowerShellを起動して以下を実行。
```
> Install-Module -Name ExchangeOnlineManagement
> Import-module ExchangeOnlineManagement
> Connect-ExchangeOnline -Organization "ディレクトリ (テナント) ID"
認証ウィンドウが開くのでAzure AD管理者でログインして以下を実行。
> New-ServicePrincipal -AppId "アプリケーション (クライアント) ID" -ServiceId "エンタープライズアプリケーションのオブジェクト ID"
> Get-ServicePrincipal | fl
> Add-MailboxPermission -Identity "(Azure ADアカウント)" -User "エンタープライズアプリケーションのオブジェクト ID" -AccessRights FullAccess
> Get-MailboxPermission -Identity (Azure ADアカウント)
```
なお、New-ServicePrincipal以降のコマンドは管理権限の無い一般ユーザでは実行出来ない(正確には'CommandNotFoundException'となりコマンドの存在が隠される)。


# 2. Postfix, Cyrus SASLインストール
```
$ sudo yum install postfix mailx cyrus-sasl cyrus-sasl-lib cyrus-sasl-plain
$ sudo systemctl start postfix saslauthd
$ sudo systemctl enable postfix saslauthd
$ systemctl status postfix saslauthd
```
RHEL 8/9 はsasl-xoauth2パッケージ(epelリポジトリに含まれる)をcyrus-saslと同時にインストールすれば良く、次項のビルドは不要。

# 3. [Amazon Linux 2] sasl-xoauth2のビルド、設定

#### 3-1. ビルドとsasl-xoauth2-toolの動作に必要なパッケージの追加

```
## rpmパッケージ
$ sudo yum install cmake3 pandoc libcurl-devel jsoncpp-devel cyrus-sasl-devel
```
Python 3のモジュールを追加する。Amazon Linux 2はurllib3のダウングレードが必要。
```
## python 3モジュール
$ sudo pip3 install argparse-manpage
$ sudo pip3 install msal

## [Amazon Linux 2] urllib3のダウングレード(OpenSSLのバージョン依存のため)
$ sudo pip3 install urllib3==1.26.6
```

#### 3-2. ソースのビルド、インストール
```
$ git clone https://github.com/tarickb/sasl-xoauth2.git
$ cd sasl-xoauth2 ; pwd
$ mkdir build && cd build && cmake3 ..
$ make
$ sudo make install
```
cmake3の引数でインストール先を指定していないため/usr/local/以下にインストールされる。


#### 3-3. Cyrus SASLにプラグインを登録
```
## ビルドしたsasl-xoauth2ライブラリの配置
$ sudo ln -s /usr/local/lib64/sasl2/libsasl-xoauth2.so /usr/lib64/sasl2/libsasl-xoauth2.so
$ ls -l /usr/local/lib64/sasl2/libsasl-xoauth2.so /usr/lib64/sasl2/libsasl-xoauth2.so

## プロセス再起動
$ sudo systemctl restart saslauthd
$ systemctl status saslauthd

## Cyrus SASLプラグインの確認
$ /usr/sbin/pluginviewer -c
(出力例, 抜粋)
Plugin "sasl-xoauth2" [loaded],         API version: 4
        SASL mechanism: XOAUTH2, best SSF: 60
        security flags: NO_ANONYMOUS|NO_PLAINTEXT|PASS_CREDENTIALS
        features: WANT_CLIENT_FIRST|PROXY_AUTHENTICATION
```


# 4. アクセストークン取得、SMTP認証確認

#### 4-1. アクセストークン取得
```
## sasl-xoauth2.confのsymlink作成
$ sudo ln -s /usr/local/etc/sasl-xoauth2.conf /etc/sasl-xoauth2.conf
$ ls -l /usr/local/etc/sasl-xoauth2.conf /etc/sasl-xoauth2.conf

## sasl-xoauth2.conf修正
sudo vi /etc/sasl-xoauth2.conf

{
  "client_id": "アプリケーション (クライアント) ID",
  "client_secret": "",
  "token_endpoint": "https://login.microsoftonline.com/(ディレクトリ (テナント) ID)/oauth2/v2.0/token"
}

## アクセストークンの取得
$ sudo mkdir /etc/tokens/
$ sudo /usr/local/bin/sasl-xoauth2-tool get-token outlook \
    /etc/tokens/(保存先トークンファイル名 = Azure ADアカウント) \
    --tenant=(ディレクトリ (テナント) ID) \
    --client-id=(アプリケーション (クライアント) ID) \
    --use-device-flow
```
>To sign in, use a web browser to open the page https://microsoft.com/devicelogin and enter the code xxxxxxxxx to authenticate.

と表示されるのでWebブラウザでURLを開き認証する。Azure ADアカウントはメール送信者を選択。認証が成功し(トークンファイルが更新され)"Acquired token."と出力されることを確認。

トークンファイルの書式は以下の通り。
```
{
    "access_token": "aaaaa",　　# 実際は約2280バイトの英数記号
    "refresh_token": "rrrrr",　　# 実際は約820バイトの英数記号
    "expiry": 0
}
```
トークンファイルはpostfixユーザのみアクセス可能とする。
```
## トークンファイルのアクセス権設定
$ sudo chown -R postfix:postfix /etc/tokens
$ sudo chmod -R o-r /etc/tokens
$ sudo ls -ld /etc/tokens /etc/tokens/*
```


#### 4-2. SMTP認証の確認
```
## ユーザ+トークンをBase64エンコード
$ echo -en 'user=(Azure ADアカウント)\001auth=Bearer (アクセストークンの値)\001\001' | base64 -w0; echo

## opensslでM365 SMTPサーバに接続
$ openssl s_client -connect smtp.office365.com:587 -starttls smtp -crlf -ign_eof
ehlo test
AUTH XOAUTH2
```
334の応答の後にユーザ+トークンをBase64エンコードしたものを貼り付ける。"Authentication successful"と返れば認証成功。quitで切断。

※認証失敗時の原因

1. "Authentication unsuccessful"
・Base64エンコードの失敗、コピペミス
・「アプリのサービス プリンシパルをExchangeに登録」が未実行(アプリがメールボックスにアクセスする権限が無い)
・アクセストークンの有効期限切れ(規定値: 60分)

2. "Authentication unsuccessful, SmtpClientAuthentication is disabled for the Mailbox"
Azure AD管理者が[M365管理センター](https://portal.office.com/Adminportal/)で該当ユーザの「メールアプリを管理する」で"認証済みSMTP"を無効にしている。


# 5. Postfix設定、メール送信確認

#### 5-1. main.cf
```
$ sudo vi /etc/postfix/main.cf

(Amazon Linux 2の場合。RHEL 8 は登録済み)
smtp_tls_CApath = /etc/pki/tls/certs
smtp_tls_CAfile = /etc/pki/tls/certs/ca-bundle.crt
smtp_tls_security_level = may

(以下、Amazon Linux 2 / RHEL 8 共通)
smtp_tls_loglevel = 1
smtp_tls_session_cache_database = btree:${data_directory}/smtp_scache
smtp_sasl_auth_enable = yes
smtp_sasl_type = cyrus
smtp_sasl_password_maps = hash:/etc/postfix/sasl_passwd
smtp_sasl_security_options = noanonymous
smtp_sasl_mechanism_filter = xoauth2
# Per-sender relay
sender_dependent_relayhost_maps = hash:/etc/postfix/relayhost_map
```

#### 5-2. sasl_passwd
```
$ sudo vi /etc/postfix/sasl_passwd

[smtp.office365.com]:587 (Azure ADアカウント):/etc/tokens/(トークンファイル = Azure ADアカウント)

$ sudo postmap hash:/etc/postfix/sasl_passwd
$ sudo chmod 600 /etc/postfix/sasl_passwd*
$ ls -l /etc/postfix/sasl_passwd*
```

#### 5-3. relayhost_map
```
$ sudo vi /etc/postfix/relayhost_map

(Azure ADアカウント) [smtp.office365.com]:587

$ sudo postmap hash:/etc/postfix/relayhost_map
$ sudo chmod 600 /etc/postfix/relayhost_map*
$ ls -l /etc/postfix/relayhost_map*
```

#### 5-4. postfix再起動
```
$ sudo systemctl restart postfix
$ systemctl status postfix
```

#### 5-5. メール送信テスト
M365への転送が出来ているか/var/log/maillogを確認。


# (補記)アクセストークンの更新
・M365へメールを転送する度にアクセストークンが更新される
・アクセストークンが有効期限切れの場合、同時に更新トークン(refresh token)も更新される
・あるホストで"sasl-xoauth2-tool get-token"を実行しても他ホストで取得済みのアクセストークンの動作に影響しない
・Azure ADアカウントのパスワードを変更するとアクセストークン、更新トークンとも更新されなくなる(ただし、アクセストークンの有効期間中はメール送信可能)。この場合、"sasl-xoauth2-tool get-token"の再実行が必要となる。
・更新トークンの有効期間である90日以上メールを送信しない場合も"sasl-xoauth2-tool get-token"の再実行が必要になると思われる(未確認)


# 参考URL
OAuth を使用して IMAP、POP、SMTP 接続を認証する
https://learn.microsoft.com/ja-jp/exchange/client-developer/legacy-protocols/how-to-authenticate-an-imap-pop-smtp-application-by-using-oauth
英文でも段落の繋ぎなど意味が分からない箇所が複数あるので流し読み程度で。

sasl-xoauth2
https://github.com/tarickb/sasl-xoauth2
Postfixの設定が目的ならこちらのREADMEを確認するべし。