ncos1のブログ

適当にメモ代わりに残しています、間違い・説明不足等はコメントしてただければ修正・追記します。

Raspberrypiからの起動メール送信にアプリパスワードを使わないOAuth 2.0認証トークン方式を検証

音楽サーバー起動時・KODIの起動時・ライブラリ更新完了時等にメールを送信しているが、現在はGoogleのアプリパスワードを使っておりセキュリティ上は推奨されないようなので別の方法を考えてみる。

 

うろ覚えの知識で、SNMP使えばアプリパスワード使わずに送れるのでは?

SNMPセットアップ。

(見当違いだった、結局今回はSNMP使わないので今後のためにざっくりメモだけ)

sudo apt update
sudo apt install snmp snmpd snmptrapd bsd-mailx postfix

初期値 インターネットサイトのままで良い。

初期値 ホスト名 のままで良い。

SNMPリスタート

sudo systemctl restart snmpd

 

テストメール

 echo "これはRaspberry Piからのテストメールです。" | mail -s "Test Mail" ***@gmail.com

NG、調べてみると結局アプリパスワードが必要っぽいので別の方法を探す。

OAuth 2.0 で認証トークンを取得する方法で良さそう。

 

ここからが本題

 

OAuth 2.0 の仕組み
OAuth 2.0では、Raspberry PiにGmailのパスワードを直接入力しない。

代わりに「Googleが発行した、メール送信権限だけを持つ一時的な許可証(アクセストークン)」を取得。

認可の要求: Raspberry Piが「自分の代わりにメールを1通送りたい」と該当メールアドレスに要求。

本人の許可: スマホやPCのブラウザで「許可する」。

トークンの発行: GoogleからRaspberry Piに、有効期限1時間の「アクセストークン」と、それを自動更新するための「リフレッシュトークン」が渡される。

メールの送信: Raspberry Piは、メールを送るたびにこのトークンをGoogleのSMTPサーバー(ポート587)に提示して認証をパスする。

有効期限が過ぎても、同一のRaspberrypiであれば「アクセストークン」はユーザー操作不要(コマンド実行は必要なのでスクリプトで実施)で更新が可能。

「アクセストークン」が盗まれても発行したRaspberrypiでなければ、更新後1時間で使えなくなるのでセキュリティが保たれる。

「リフレッシュトークン」が盗まれたとしても、個体IDと紐づけされているので使えない。

制限時間をすぎると手続きした機器でないと使えないので、ちょっと違うがPINコードをイメージすれば良さそう。

アプリパスワードは漏れたらどうしようもないので、セキュリティを考えるとOAuth 2.0にした方が良い。(SNMPでもOAuth 2.0使えば可能っぽいが、今回はできるだけパッケージ追加をしない方法で考える事にした。)

 

まずはテストしてみて問題なければ、KODIのアドオンにも組み込みたい。

 

Google Cloud Platformにアクセスして鍵を作成する。

console.cloud.google.com

 

画面上部のプロジェクト選択から 「プロジェクトの作成」 を選び、適当な名前(例: Pi-Mail-Test)で作成。

既存のプロジェクトが選択されている場合は、既存のプロジェクトをクリックすると新しいプロジェクトを作成できる。

 

既にVolumioがある、Youtubeのプラグインで作成した?手順を見る限り作成していないがプラグイン側で作成処理した?

もっと以前にYoutubeプラグインを有効化する時に作成したのかも?覚えていないので一度削除したほうが良いかな?

ncos1.hatenablog.com

 

プロジェクトを選択する。

「APIとサービス」を開く
画面中央下の「クイックアクセス」内にある 「API API とサービス」 のパネルをクリック。
(または、画面左上の 「三本線マーク(メニュー)」 をクリックし、出てきたリストから「APIとサービス」を選択しても同じ画面へ進める)

「OAuth 同意画面」の設定
移動した画面の左側メニューにある 「OAuth 同意画面」 をクリック。

開始をクリック。

「アプリ情報の登録」画面に入力。

アプリ名: Pi_Mail(任意の名前)

ユーザーサポートメール: 選択肢からご自身のGmailアドレスを選択

デベロッパーの連絡先情報: ご自身のGmailアドレスを入力

入力後、画面下部の 「保存して次へ」 をクリック。

外部 を選択。
(「内部」は、企業や学校などで独自ドメインのGoogle Workspaceアカウントを一括管理している組織向けの機能)

次へをクリック、連絡先メールアドレスを入力。

ポリシーに同意して作成をクリック。

 

実際にRaspberry Piへ組み込むための鍵(クライアントIDとシークレット)を発行する手順。

OAuth クライアントを作成  ボタンをクリック

もしくは、 クライアント クリック後に クライアント作成をクリック

アプリケーションの種類をクリックして、デスクトップアプリを選択。

名前: 自動的に名前(例: デスクトップ クライアント 1 など)が入力さる(エンドユーザーには表示され)のでそのまま作成をクリック。

この画面が表示されるので、クライアントシークレットを控える事。

JSONをダウンロードすれば中に書いてあるが、個別で管理したほうが見やすい。。

「Google Cloudでの鍵作り」まで完了。



ブラウザを使って、その鍵に対して「メール送信権限」を一度だけ手動で許可し、Raspberry Piで自動更新するための「リフレッシュトークン」と、テスト送信用の「アクセストークン」の2つを取得する手順に進む。

手順1:ブラウザでアクセスして「認可コード」を出す。

*Raspberrypiで行う必要はないので、動作が軽いWindows等で実施可能。


PCやスマホのブラウザを使い、以下のURLの 【クライアントID】 の部分を実際のクライアントID(控えたもの)に書き換えて、アドレスバーに貼り付けてアクセス。


https://accounts.google.com/o/oauth2/v2/auth?client_id=【クライアントID】&redirect_uri=http://localhost&response_type=code&scope=https://mail.google.com/&access_type=offline&prompt=consent

アクセスして使いたいアカウントを選択。

 

「Googleがこのアプリを検証していません」という警告画面(英語または日本語)が出るが、自分で作ったテストアプリなので安全なので先に進める。
画面内の 「詳細」(Advanced)をクリックし、リンク先の 「Go to アプリ名 (unsafe)」(または「MyPi(安全ではないページ)に移動」)をクリック。

次の画面で「メールの閲覧、送信、削除…」へのアクセスを求めるチェックボックスが表示るので、これに必ずチェックを入れて許可し、進める。

NG、進めない。

Google Cloud Platform で修正が必要だった。

対象・テストユーザー¥+ ADD USERSで、使いたいGoogleアカウントを追加。

再度アクセスすると、 続行 が選択できる。

続行をクリックすると今回作成した、Pi_Mailにアクセス権を付与できる。

続行した後は、停止するがRaspberry Piの中にWebサーバーを建てていないため今回の手順では正常。

表示されているエラー画面のまま、ブラウザの一番上にある「アドレスバー(URL欄)」 を確認。

URLの末尾に code= から始まる長い文字列が表示されている。

この code= から &scope の手前までの長い文字列が 「認可コード」となる。

 

認可コードを使って「本番用の暗号鍵(トークン)」を請求する。

Raspberrypiからコマンド入力。

curl -X POST https://oauth2.googleapis.com/token -d "code=【取得した認可コード】" -d "client_id=【クライアントID】" -d "client_secret=【クライアントシークレット】" -d "redirect_uri=http://localhost" -d "grant_type=authorization_code"

(認可コード≠有効期限1時間の「アクセストークン」 で「アクセストークン」は数分間の有効期限なので   "error": "invalid_grant",  "error_description": "Bad Request" で弾かれた場合は認可コードを再取得して再度手続きする。)

実行すると鍵が作成されるのでコピーしておく。

 

  "expires_in": 3599,
  "refresh_token": "***** 作成された鍵が表示される *****",
  "scope": "https://mail.google.com/",
  "token_type": "Bearer",
  "refresh_token_expires_in": 604799
}

 

"refresh_token":以降をテキストでコピーして保存。

 

このリフレッシュトークン(親鍵)さえ保存しておけば、1時間で切れるアクセストークン(子鍵)を自動で何回でも作り直せるようになる。

 

以前のPythonで送る方法より swaks をインストールしたほうが簡単らしいのでswaksをインストール。

sudo apt update && sudo apt install -y swaks jq

テスト

swaks --server smtp.gmail.com --port 587 --tls --from 【送信元Gmail】 --to 【受信先メール】 --auth-options USER=【送信元Gmail】 --auth-type XOAUTH2 --auth-password $(curl -s -X POST https://oauth2.googleapis.com/token -d "grant_type=refresh_token" -d "client_id=【クライアントID】" -d "client_secret=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" -d "refresh_token=【取得したrefresh_token】" | jq -r .access_token) --header "Subject: swaks Test" --body "swaksからのテスト送信です。"

失敗、Raspberrypiでaptでインストールしたswaks はOAuth 2.0認証に対応していないっぽい?ので従来の方法で試みる。

 

変更前、アプリパスワードを使用する場合

#!/usr/bin/env python3
import smtplib
from email.mime.text import MIMEText
from email.utils import formatdate
import sys

# ==========================================
# 設定項目
# ==========================================
# SMTPサーバー設定
SMTP_SERVER = 'smtp.gmail.com'
SMTP_PORT = 465 # SSL用のポート。STARTTLSの場合は 587
SENDER_EMAIL = '●●●●@gmail.com' # 送信元
APP_PASSWORD = '●●●● ●●●● ●●●● ●●●●' # アプリパスワード(16桁)

def send_email(subject, body, receiver):
    # メッセージの構築
    msg = MIMEText(body, 'plain', 'utf-8')
    msg['Subject'] = subject
    msg['From'] = SENDER_EMAIL
    msg['To'] = receiver
    msg['Date'] = formatdate(localtime=True)

    try:
        # SMTP_SSL を使用してサーバーに接続
        with smtplib.SMTP_SSL(SMTP_SERVER, SMTP_PORT) as smtp:
            # ログイン(アプリパスワードを使用)
            smtp.login(SENDER_EMAIL, APP_PASSWORD)
            # メールの送信
            smtp.send_message(msg)
        print("メールが正常に送信されました。")
        return True
    except Exception as e:
        print(f"エラーが発生しました: {e}", file=sys.stderr)
        return False

if __name__ == '__main__':
    # 引数のチェック
    if len(sys.argv) < 3:
        print("使用法: python3 sendmail.py \"件名\" \"本文\" [宛先メールアドレス]", file=sys.stderr)
        print("※ 宛先を省略した場合は送信元(自分)に送信します。", file=sys.stderr)
        sys.exit(1)

    # 引数の取得
    subject_arg = sys.argv[1]
    body_arg = sys.argv[2]
    
    # 第3引数があればそれを宛先に、なければ送信元を宛先にする
    receiver_arg = sys.argv[3] if len(sys.argv) > 3 else SENDER_EMAIL

    # メールの送信
    send_email(subject_arg, body_arg, receiver_arg)

 

アプリパスワードを使用する方法、リフレッシュトークンからアクセストークンを自動取得し、XOAUTH2認証で送信する。

sendmail_test.py で作成

#!/usr/bin/env python3
import smtplib
from email.mime.text import MIMEText
from email.utils import formatdate
import sys
import urllib.request
import urllib.parse
import json
import base64

# ==========================================
# 設定項目
# ==========================================
# SMTPサーバー設定(GmailのOAuth2認証はSTARTTLS方式を使用するため、ポート587に固定)
SMTP_SERVER = 'smtp.gmail.com'
SMTP_PORT = 587

# 各変数には環境に合わせた認証情報を文字列として代入。
CLIENT_ID = 'ここにGoogle Cloudで取得したクライアントIDを記述'
CLIENT_SECRET = 'ここにGoogle Cloudで取得したクライアントシークレットを記述'
REFRESH_TOKEN = 'ここにCURL等で事前に取得した1行の長いリフレッシュトークンを記述'
SENDER_EMAIL = 'ここに送信元のGmailアドレスを記述'

def get_access_token():
    """リフレッシュトークンを使用して最新のアクセストークンを取得する"""
    url = 'https://oauth2.googleapis.com/token'
    data = urllib.parse.urlencode({
        'grant_type': 'refresh_token',
        'client_id': CLIENT_ID,
        'client_secret': CLIENT_SECRET,
        'refresh_token': REFRESH_TOKEN
    }).encode('utf-8')
    
    req = urllib.request.Request(url, data=data, method='POST')
    with urllib.request.urlopen(req) as response:
        res_data = json.loads(response.read().decode('utf-8'))
        return res_data['access_token']

def send_email(subject, body, receiver):
    # メッセージの構築
    msg = MIMEText(body, 'plain', 'utf-8')
    msg['Subject'] = subject
    msg['From'] = SENDER_EMAIL
    msg['To'] = receiver
    msg['Date'] = formatdate(localtime=True)

    try:
        # 1. アクセストークンの調達
        access_token = get_access_token()

        # 2. XOAUTH2 認証文字列の生成
        auth_string = f"user={SENDER_EMAIL}\x01auth=Bearer {access_token}\x01\x01"
        auth_b64 = base64.b64encode(auth_string.encode('utf-8')).decode('utf-8')

        # 3. SMTPサーバーへの接続とSTARTTLS暗号化の開始
        with smtplib.SMTP(SMTP_SERVER, SMTP_PORT) as smtp:
            smtp.ehlo()
            smtp.starttls()
            smtp.ehlo()
            
            # XOAUTH2コマンドを発行して認証を実行
            status_code, response = smtp.docmd('AUTH', f'XOAUTH2 {auth_b64}')
            
            # 認証に失敗した場合は例外をスロー
            if status_code != 235:
                raise smtplib.SMTPAuthenticationError(status_code, response)
                
            # メールの送信
            smtp.send_message(msg)
            
        print("メールが正常に送信されました。")
        return True
    except Exception as e:
        print(f"エラーが発生しました: {e}", file=sys.stderr)
        return False

if __name__ == '__main__':
    # 引数のチェック
    if len(sys.argv) < 3:
        print("使用法: python3 sendmail_test.py \"件名\" \"本文\" [宛先メールアドレス]", file=sys.stderr)
        print("※ 宛先を省略した場合は送信元(自分)に送信します。", file=sys.stderr)
        sys.exit(1)

    # 引数の取得
    subject_arg = sys.argv[1]
    body_arg = sys.argv[2]
    
    # 第3引数があればそれを宛先に、なければ送信元を宛先にする
    receiver_arg = sys.argv[3] if len(sys.argv) > 3 else SENDER_EMAIL

    # メールの送信
    send_email(subject_arg, body_arg, receiver_arg)

 

使用法: sendmail_test.py "件名" "本文" [宛先メールアドレス]

 

python sendmail_test.py test test ****@gmail.com

成功した、KODIのアドオンに組み込むか検討しよう。

 

Raspberrypi本体を変更した際には、再度リフレッシュトークンの取得が必要なので正直面倒なので別の方法を考えたい。