はじめに
Microsoftからの「Exchange OnlineにおけるSMTP AUTHのサポート終了」というお知らせ
をみて自身で開発しているアプリからのメール通知をOAuth2認証に対応しなければならないと困っている人がいると思います。私もTWSNMP FC
や TWSNMP FK
というGo言語で開発したソフトにメール通知機能があり、これらのソフトをOAuth2認証に対応した時のポイントを紹介しようと思います。
OAuth2認証対応のポイント
Gemini(AI)にポイントを聞いてみました。
Go言語でOAuth 2.0認証を使ってSMTPでメールを送信するには、主に以下の手順が必要です。
1.OAuth 2.0アクセストークンの取得:
メールサービスプロバイダー(Gmail、Office 365など)の開発者コンソールでアプリケーションを登録し、クライアントIDとクライアントシークレットを取得します。
ユーザーの同意を得て(通常はウェブブラウザ経由)、認可コードを取得します。
取得した認可コードとクライアントシークレットを使って、プロバイダーのトークンエンドポイントからアクセストークンを取得します。SMTPで利用する場合は、多くの場合、オフラインアクセス用のoffline_accessスコープと、メール送信権限を与えるスコープ(例: Gmailの場合は https://mail.google.com/)が必要です。
Go言語の標準ライブラリの golang.org/x/oauth2 パッケージがトークン取得プロセスに役立ちます。
2.SASL XOAUTH2形式の文字列生成:
SMTPでOAuth 2.0認証(XOAUTH2メカニズム)を使用する場合、アクセストークンとユーザー名を特定の形式(user=<ユーザー名>^Aauth=Bearer <アクセストークン>^A^A、ここで^AはControl-A、つまり\x01)で結合し、それをBase64エンコードした文字列を作成する必要があります。
この処理を行うためのGoライブラリ(例: github.com/sqs/go-xoauth2)を利用するか、自分で実装します。
3.SMTP接続と認証:
Go言語の標準ライブラリ net/smtp パッケージを使います。
smtp.PlainAuth の代わりに、SASL XOAUTH2認証を実装したカスタムの smtp.Auth インターフェースを実装します。このカスタム実装が、SMTPサーバーに「AUTH XOAUTH2」コマンドと、ステップ2で生成したBase64エンコードされた文字列を送信します。
net/smtp パッケージの SendMail 関数や、より高度なメール送信ライブラリ(例: gopkg.in/mail.v2)で、カスタムの smtp.Auth を使用してメールを送信します。
マインドマップに描いて整理すると
OAuth 2.0アクセストークンの取得
OAuth2の参考サイトは
Googleの場合
Mictosoft場合
です。
GoogleのPythonのサンプルコードを実行すれば、アクセストークンを取得できます。
クライアントIDの取得は、Google Cloud Consoleで
のように設定します。リダイレクトのURIをプログラムと合わせるのがポイントです。
違っているとエラーになります。
Go言語で同じことを実現するプログラムは、
package main
import (
"context"
"fmt"
"log"
"net/http"
"golang.org/x/oauth2"
"golang.org/x/oauth2/google" // Googleサービスの場合
)
var (
// Google Cloud Consoleなどで取得した情報に置き換えてください
clientID = "YOUR_CLIENT_ID"
clientSecret = "YOUR_CLIENT_SECRET"
redirectURL = "http://localhost:8180/callback" // アプリケーションのコールバックURL
// 必要な権限スコープ (例: Gmailの全アクセス権限)
// SMTP送信に必要なスコープは通常 "https://mail.google.com/"
scopes = []string{"https://mail.google.com/"}
)
func getConfig() *oauth2.Config {
return &oauth2.Config{
ClientID: clientID,
ClientSecret: clientSecret,
// Googleサービスの場合、google.Endpointを使用
Endpoint: google.Endpoint,
RedirectURL: redirectURL,
Scopes: scopes,
}
}
func getAuthCode() string {
config := getConfig()
// StateはCSRF対策のためのランダムな文字列。ここではシンプルにするため固定値
state := "randomstate"
// 認可URLを生成
url := config.AuthCodeURL(state, oauth2.AccessTypeOffline, oauth2.ApprovalForce)
fmt.Printf("以下のURLをブラウザで開いて認証を完了してください:\n%s\n", url)
// ユーザーが認証後にリダイレクトされるのを待つHTTPサーバーを設定
// 実際のアプリケーションでは、この部分はウェブフレームワークで実装されます
authCodeChan := make(chan string)
http.HandleFunc("/callback", func(w http.ResponseWriter, r *http.Request) {
// Stateの検証
if r.FormValue("state") != state {
http.Error(w, "State mismatch", http.StatusBadRequest)
return
}
// 認可コードを取得
code := r.FormValue("code")
if code == "" {
http.Error(w, "Authorization code not found", http.StatusBadRequest)
return
}
// ユーザーに認証成功を伝え、コードをチャネルに送信
fmt.Fprintf(w, "認証が完了しました。このウィンドウを閉じてください。")
authCodeChan <- code
})
// サーバーをバックグラウンドで起動
go http.ListenAndServe(":8180", nil)
// 認可コードがチャネルに送られるのを待つ
return <-authCodeChan
}
func getTokenFromWeb(code string) (*oauth2.Token, error) {
config := getConfig()
ctx := context.Background()
// 認可コードを使ってトークンを取得
token, err := config.Exchange(ctx, code)
if err != nil {
return nil, fmt.Errorf("トークンの取得に失敗しました: %w", err)
}
return token, nil
}
func main() {
// 1. 認可コードを取得
authCode := getAuthCode()
// 2. 認可コードからトークンを取得
token, err := getTokenFromWeb(authCode)
if err != nil {
log.Fatalf("トークン取得エラー: %v", err)
}
fmt.Println("\n--- 成功 ---")
fmt.Printf("アクセストークン: %s\n", token.AccessToken)
// リフレッシュトークンが存在する場合、永続化して将来的に再利用する必要があります
if token.RefreshToken != "" {
fmt.Printf("リフレッシュトークン (永続化が必要です): %s\n", token.RefreshToken)
} else {
fmt.Println("リフレッシュトークンは取得されませんでした (oauth2.AccessTypeOffline が必要です)。")
}
// 💡 次のステップ: このアクセストークンを
// カスタムの XOAUTH2実装に渡し、SMTPメールを送信します。
}
メール送信の設定画面
Webやデスクトップアプリに組み込む場合には、設定画面が必要です。
TWSNMP FKの場合には
のような画面にしました。
プロバイダーを選択するとOAuth2やメール送信に必要なパラメータの入力画面を表示します。
クライアントID,クライアントシークレット、ユーザー(アカウント)です。
Micsrosoftの場合は、テナントIDが必要でした。
設定を保存したら<トークン取得>ボタンをクリックして、最初のトークン取得を行います。
先程のテストプログラムでは
「以下のURLをブラウザで開いて認証を完了してください」
のメッセージで表示されるURLにアクセスすれば同じ画面が表示されます。
のようにブラウザーでアカウントの選択画面が表示されます。ログインしていない場合は、
ログイン画面が表示されます。<続行>をクリックして、確認画面がでます。
最後に
アクセスを許可すれば、トークンが発行されます。
アクセストークンを使ってメールを送信する
Go言語の標準のnet/smtpパッケージでは、OAuth2の認証に対応していません。
対応したパッケージとして
があります。これを使えば簡単に対応できます。
テストコードは、
package main
import (
"log"
"github.com/wneessen/go-mail"
)
func main() {
username := "<Gamilのアカウント(メールアドレス)>"
password := "<取得したOAuth2のトークン>"
client, err := mail.NewClient("smtp.gmail.com",
mail.WithTLSPortPolicy(mail.TLSMandatory),
mail.WithSMTPAuth(mail.SMTPAuthXOAUTH2),
mail.WithLogAuthData(),
mail.WithDebugLog(),
mail.WithUsername(username), mail.WithPassword(password))
if err != nil {
log.Fatalf("failed to create mail client: %s\n", err)
}
message := mail.NewMsg()
if err := message.From("<送信元のメールアドレス>"); err != nil {
log.Fatalf("failed to set From address: %s", err)
}
if err := message.To("<宛先のメールアドレス>"); err != nil {
log.Fatalf("failed to set To address: %s", err)
}
message.Subject("This is my first mail with go-mail!")
message.SetBodyString(mail.TypeTextPlain, "Do you like this mail? I certainly do!")
if err := client.DialAndSend(message); err != nil {
log.Fatalf("failed to send mail: %s", err)
}
}
テストする時は、username,送信元、宛先を、同じgmailのアドレスにするのが
よいと思います。
OAuth 2.0アクセストークンの更新
Googleの場合アクセストークンは、1時間ぐらいで無効になってしまいます。トークンを更新する必要があります。更新は、リフレッシュ用のトークンを使って行います。
トークンのリフレッシュ処理は、
// persistedToken: DBなどに保存されていた前回の認証情報
func refreshAccessToken(persistedToken *oauth2.Token) (*oauth2.Token, error) {
ctx := context.Background()
config := getConfig()
// 1. Saved Token (persistedToken) から TokenSource を作成します。
// この TokenSource は、トークンが期限切れの場合にリフレッシュトークンを使用して自動的にリフレッシュを試みます。
tokenSource := config.TokenSource(ctx, persistedToken)
// 2. TokenSource.Token() を呼び出すと、以下の処理が行われます。
// - トークンがまだ有効な場合は、そのトークンを返す。
// - トークンが期限切れの場合は、リフレッシュトークンを使用して新しいトークンを自動で取得する(リフレッシュリクエスト)。
// - 新しいトークンが取得されると、そのトークンを返す。
newToken, err := tokenSource.Token()
if err != nil {
return nil, fmt.Errorf("TokenSourceによるトークンの取得またはリフレッシュに失敗しました: %w", err)
}
// 3. ⚠️ 重要: 新しいトークン(newToken)には、新しい有効期限と、
// 更新されたリフレッシュトークン(もしプロバイダーが更新した場合)が含まれています。
// この新しいトークン全体をデータベースやファイルに保存し直す必要があります。
// (リフレッシュトークンは古いものと同じである場合もあります。)
return newToken, nil
}
です。取得したトークンを保存しておくのがポイントです。この処理の場合、トークンが有効ならば更新を行わず、そのまま使います。
なんとなく、トークンを定期的にリフレッシュする処理が必要なように思いますが、メールを送信する前に、このリフレッシュの処理を実行すれば、必要な時だけリフレッシュされます。




