LoginSignup
6
8

More than 1 year has passed since last update.

Exchange OnlineでOAuth 2.0認証でメールを受信する

Last updated at Posted at 2020-10-28

前回の記事でメール受信できたけど、結局業務で日の目を見ませんでした。
約1年が経って、新たにメール受信するアプリ開発の話がでて、楽勝だと安請け合いしたら世の中はこんなことになってたのね。。。

どうせ1年もしないうちに改造が必要なら、最初から対応しておきたいのが人情。
事前に調べたらExchange Online側を弄る必要があるとのことで、現運用環境を使うわけには行かず、1ヶ月間無料のExchange Onlineを申し込んで、OAuth 2.0に基づく先進認証なるものでメールの受信を試してみました。

ちなみに開発目的のアプリは、定期的にアプリ専用のメールアカウントで受信したメールをテキストファイルに出力する常駐アプリで、認証方法としては"使用しない"ことを勧められているROPCフローを想定しているため、以下はその実装例となってます。

【2022/7/5 注記】
ROPCフローであれば下記にあるエンドポイントは https://login.microsoftonline.com/.../oauth2/v2.0/authorize ではなく、https://login.microsoftonline.com/.../oauth2/v2.0/token ではないかとのご指摘をいただきました。
https://docs.microsoft.com/ja-jp/azure/active-directory/develop/v2-oauth-ropc を読むと /token のようなのですが、当時は /authorize で動作できており、現在はどうなのかを確認できる環境にいないため、記事自体の修正は行わず、この記述をもって注意喚起とさせていただきます。
@nerocrux さんありがとうございます。

アプリケーションの登録

大まかな処理の流れと、この手順はチュートリアルを参考にしました。

  1. Azure Active Directory 管理センターにアプリがメール受信で使用するアカウントでログインし、[Azure Active Directory]-[アプリの登録]-[新規登録]をクリック。
    image.png

  2. アプリの名前を入力。今回はアプリを使用するのは特定のアカウントのみなので[この組織ディレクトリのみに含まれるアカウント]を選択。リダイレクトURIは使用しないから触らない。そして[登録]をクリック。
    image.png

  3. [パブリック クライアント フローを許可する]を[はい]にして、[保存]をクリック。
    image.png

  4. [APIのアクセス許可]の[アクセス許可の追加]で、[Microsoft Graph]の[委任されたアクセス許可]から[IMAP.AccessAsUser.All]を追加。今回はログインしているアカウントが管理者でもあるので、そのまま[〜に管理者の同意を与えます]をクリック。
    image.png

  • プロトコルでPOPを使用するなら[POP.AccessAsUser.All]を許可する。
  • ここでは別途、送信も試すので[SMTP.Send]を許可している。
  • ログインしているアカウントが管理者でなければ(大抵はそうでしょうけど)、管理者に同意を依頼することになる。管理者アカウントでログインし、[アプリの登録]で[すべてのアプリケーション]を選択して該当のアプリを操作してもらうことになる。

基礎となるアプリケーションの作成

簡素化するためにSpring Boot 2.3.4とKotlinを使用しました。
spring initializerの依存関係で、Java Mail Sender (つまりspring-boot-starter-mail)のみ追加して、プロジェクト作成後のpom.xmlへMicrosoft Authentication Library for Javaを追加。

pom.xml
<dependency>
  <groupId>com.microsoft.azure</groupId>
  <artifactId>msal4j</artifactId>
  <version>1.7.1</version>
</dependency>

CommandLineRunnerを実装し、runメソッドの中に処理を書いて行きます。

@SpringBootApplication
class SampleMailApplication : CommandLineRunner {
  override fun run(vararg args: String?) {
    // 実装
  }
}

アクセストークン取得

上記で参照したチュートリアルのこの手順がイケてません。なんだ「App.xamlを開き」って、C#の記事の残りかよ。。。
しかたなくネットで調べて、以下のコードで動くようになりました。

import com.microsoft.aad.msal4j.PublicClientApplication
import com.microsoft.aad.msal4j.UserNamePasswordParameters

// 割愛
override fun run(vararg args: String?) {
  // 登録したアプリの概要ページにあるアプリケーションIDを指定する
  val applicationId = "..."
  // ...部分に概要ページにあるディレクトリIDを指定する
  val authEndpoint = "https://login.microsoftonline.com/.../oauth2/v2.0/authorize"
  // 使用するプロトコルにあわせて指定する
  val scope = setOf("https://outlook.office365.com/IMAP.AccessAsUser.All",
    "https://outlook.office365.com/SMTP.Send")
  // アカウントのメールアドレスを指定する
  val username = "..."
  // アカウントのパスワードを指定する
  val password = "..."

  val pca = PublicClientApplication.builder(applicationId)
    .authority(authEndpoint)
    .build()

  val parameters = UserNamePasswordParameters
    .builder(scope, username, password.toCharArray())
    .build()
  val result = pca.acquireToken(parameters).join()
  println("アクセストークン: ${result.accessToken()}")
  • APIのアクセス許可で管理者の同意が得られていないと、acquireTokenメソッドで「com.microsoft.aad.msal4j.MsalInteractionRequiredException: AADSTS65001: The user or administrator has not consented to use the application」の例外が発生します。

メール受信

var props = Properties()
// 認証にOAuth 2.0を使用
props["mail.imaps.auth.mechanisms"] = "XOAUTH2"
var session: Session = Session.getInstance(props)
val store: Store = session.getStore("imaps")
// パスワードにアクセストークンを使用
store.connect("outlook.office365.com", 993, username, result.accessToken())
val folderInbox: Folder = store.getFolder("INBOX")
folderInbox.open(Folder.READ_ONLY)
folderInbox.messages.forEach { println("件名: ${it.subject}") }

最初のポイントはmail.imaps.auth.mechanismsで、説明によるとデフォルト値はサポートされている認証からXOAUTH2を除く全てで、Exchange Onlineでは基本認証が使われます。そこでXOAUTH2を指定してOAuth 2.0認証が行われるようにします。

次にパスワードの代わりにアクセストークンを指定するだけで、OAuth 2.0認証でメールの受信が可能になりました。

(補足)メール送信

当面継続ということは、そのうち廃止となるであろうSMTPの基本認証についても、OAuth 2.0認証を試してみたいと思います。
しかし、OAuth 2.0認証どころか、基本認証でも

javax.mail.AuthenticationFailedException: 535 5.7.3 Authentication unsuccessful

になるではありませんか!メールボックスで認証済みSMTPはオンになってるのに。。。

そのページで「認証ポリシーによって SMTP の基本認証が無効になっている場合、この記事で説明されている設定を有効にしても、クライアントは SMTP 認証プロトコルを使用できません。」とあるので[セキュリティの既定値群の有効化]を[いいえ]したらメール送信が成功するようになったのですが、今度は[セキュリティの既定値群の有効化]を[はい]に戻しても、もうエラーは起きません。(他に弄った何かが影響している可能性は否定できません。。。)

なんだか良く判らない、、、

一応、成功した送信ロジックも記載します。

val props = Properties()
props["mail.smtp.auth"] = "true"
props["mail.smtp.auth.mechanisms"] = "XOAUTH2";
props["mail.smtp.starttls.enable"] = "true"
val session = Session.getInstance(props)
val transport = session.getTransport("smtp")
transport.connect("smtp.office365.com", 587, username, result.accessToken())

val sendMessage = MimeMessage(session)
sendMessage.addRecipients(Message.RecipientType.TO, username)
sendMessage.setFrom(username)
sendMessage.subject = "送信"
sendMessage.setText("送信テスト")

transport.sendMessage(sendMessage, sendMessage.allRecipients)

なお、送信自体はできているのですが、送り先から「550 5.7.501 Service unavailable. Spam abuse detected from IP range. 」で最終的な配達まできていません。これは onmicrosoft.com ドメインからメールはスパムとしてフィルターされるからで、試しにOutlookから送信しても同じ結果になるので、ロジックとしては問題ないと思います。

6
8
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
8