[T-DASH] 多要素認証ログインでメール(gmail)をAPI経由で取得して、テスト自動化する

# Outline

サイトで新規会員登録や、ログインなどで多要素認証を行うものがある。
※URL認証なども正規表現を変えれば、今回の記事で対応可能

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/267085/07181ca1-a349-b48b-398e-41b5416676dc.png)

このような多要素認証をはさんだWEBのテストを行う場合、２つに対して自動化を組む必要がある。

- テスト対象（WEB)
- メール（gmail）

今回、gmailで受信したものに対して、認証処理を行うものとする。

## 技術的な課題

WEBのテストなので、gmailのweb siteに対してscriptを組めば解決できるとおもった。
実際にscriptを組んでみると以下のようなエラーになり、googleにloginできなかった。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/267085/d4d0c6a1-6948-fba0-0ac3-f5de6b8312f5.png)

他の記事で、自動化であることを回避する方法を試してみたが、どうしてもうまくいかない。

https://prtn-life.com/blog/selenium-chrome-login#comment-2353

以下記事によると、googleのセキュリティ強化でseleniumからはアクセスできなくなっているようだ。

https://mashable.com/article/gmail-app-developers-cybersecurity

また別の方法としてIMAPにアクセスする方法もある。
robot frameworkのImapLibraryである。
しかし、こちらで接続をしてもmail serverにもよるが、セキュリティの問題でアクセスが拒否される。
gmailはdefaultでは無理だった。
gmailの場合、”安全性の低いアプリへのアクセスを管理する”でアクセスすることも可能だが、安全性や2024年秋ではサポートされなくなる。

https://ricky.one/robotframework-imaplibrary/doc/ImapLibrary.html


https://stackoverflow.com/questions/57963552/i-encounter-an-authenticationfailed-error-when-connecting-to-email-with-imaplib



## 解決方法

gmailにAPIでアクセスして、メールの情報を取りに行く。
尚、今回は固定のメールアドレスに対して、MFA情報をとることを前提とする。
アカウントの切り替えは想定していない。
（なお、gmailのaliasを使えば、その範囲のユーザー全部のメールは対象になる）

今回、T-DASHをつかって、その自動化をやってみた

https://service.valtes.co.jp/t-dash/



# gmail APIの利用

こちらから設定を行う。
尚、Google Cloudを利用するために、有料会員設定を行う必要がある。
（90日間の無料トライアルはある）

https://developers.google.com/gmail/api/quickstart/python?hl=ja


## gmail APIを有効化

gmailをAPIで取得するために、対象のプロジェクトに対してAPIの有効化する必要がある

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/267085/94bdacbe-4c4f-dc1f-9333-efae99719fab.png)

対象のプロジェクトを作成・選択する
尚、今回デフォルトで作成されている「My First Project」を使う。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/267085/f2d4da37-b79c-7ce1-3051-881ce5734de5.png)


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/267085/3f0210c9-6231-381e-05a4-2bcaf8ac7d02.png)


## OAuthの設定


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/267085/93ab6f30-fad4-1cab-7049-b046433ef528.png)


### スコープ

使えるAPIの範囲を指定する

| API | 意味 |  
|:-----------|:------------|
| gmail.readonly       | メール メッセージと設定の表示        | 

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/267085/e0605999-319a-90c0-396d-135857930b85.png)

### テストユーザー設定

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/267085/ecfe1f92-40e5-e9e1-87a8-6105a5e6503d.png)

## 認証情報

OAuth 2.0 クライアント IDを作成する。
アプリケーションの種類として「デスクトップアプリ」を選択し、最後にJSONファイルをダウンロードする
そのjsonファイルをcredentials.jsonに名前変更する

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/267085/f2b41ba6-6fa1-f29e-8ce8-4636498e51df.png)


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/267085/b7833a28-6166-50ce-1d33-85e6ed728913.png)


# T-DASH

## シナリオ

アソビューサイトを参考にした。
新規会員登録時に、登録時のメールアドレスに認証コードがメールされる。
それを取得して認証を行う。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/267085/4b00f7ca-4beb-8012-d4a6-1d596a459c46.png)


## 構成

いくつかのファイルが今回登場する


| ファイル名 | 意味 |  
|:-----------|:------------|
|gmail.py|pythonライブラリ|
|credentials.json|OAuth Client credentials json (上記作成した、認証情報) |
|token.json|API token json (自動生成される)|

配置場所
T-DASHのcustom libraryの置き場を使う。

```
%HOMEPATH%\AppData\Local\Programs\T-DASH\customlib
```

## 独自ライブラリ

カスタム動作にinstallするpython script

gmail.py
```
import os.path
import base64
import re

from google.auth.transport.requests import Request
from google.oauth2.credentials import Credentials
from google_auth_oauthlib.flow import InstalledAppFlow
from googleapiclient.discovery import build
from googleapiclient.errors import HttpError

#from robot.libraries.BuiltIn import BuiltIn

# If modifying these scopes, delete the file token.json.
SCOPES = ["https://www.googleapis.com/auth/gmail.readonly"]

# customize 1
LIBRARY_DIR = "C:/Users/xxxxx.xxxxx/AppData/Local/Programs/T-DASH/customlib/"
CREDENTIALS_PATH = LIBRARY_DIR + "credentials.json"
TOKEN_PATH = LIBRARY_DIR + "token.json"


def get_mfa_code_from_gmail():
  """Shows basic usage of the Gmail API.
  Lists the user's Gmail labels.
  """
  # customize 2
  query = "アソビュー！より認証コードを送付します"
  tag   = "INBOX"
  count = 5
  creds = None
  # The file token.json stores the user's access and refresh tokens, and is
  # created automatically when the authorization flow completes for the first
  # time.
  if os.path.exists(TOKEN_PATH):
    creds = Credentials.from_authorized_user_file(TOKEN_PATH, SCOPES)
  # If there are no (valid) credentials available, let the user log in.
  if not creds or not creds.valid:
    if creds and creds.expired and creds.refresh_token:
      creds.refresh(Request())
    else:
      flow = InstalledAppFlow.from_client_secrets_file(
          CREDENTIALS_PATH, SCOPES
      )
      creds = flow.run_local_server(port=0)
    # Save the credentials for the next run
    with open(TOKEN_PATH, "w") as token:
      token.write(creds.to_json())

  try:
    # Call the Gmail API
    service = build("gmail", "v1", credentials=creds)
    messages = get_list_message(service, query, count=count)

    content = messages[0]['body']
    code = get_code_with_regrex(content)
    print("code:",code)
    global MFA_CODE
    MFA_CODE = code
    return code

  except HttpError as error:
    # TODO(developer) - Handle errors from gmail API.
    print(f"An error occurred: {error}")

def decode_base64url_data(data):
  """
  decode base64url
  """
  decoded_bytes = base64.urlsafe_b64decode(data)
  decoded_message = decoded_bytes.decode("utf-8")
  return decoded_message


def get_list_message(service, query, count=3):
  """
  get mail list
  """
  label_ids=['INBOX']
  messages = []
  try:
    message_ids = (
      service.users().messages().list(userId="me", maxResults=count, q=query, labelIds=label_ids).execute()
    )
    if message_ids["resultSizeEstimate"] == 0:
      print("no result data!")
      return []
    # get message with message id
    for message_id in message_ids["messages"]:
      message_detail = (
        service.users().messages().get(userId="me", id=message_id["id"]).execute()
      )
      message = {}
      message["id"] = message_id["id"]
      # plain text
      if 'data' in message_detail['payload']['body']:
        message["body"] = decode_base64url_data(message_detail["payload"]["body"]["data"])
      # html mail
      else:
        parts = message_detail['payload']['parts']
        parts = [part for part in parts if part['mimeType'] == 'text/plain']
        message["body"] = decode_base64url_data(parts[0]['body']['data'])
      # subject
      message["subject"] = [
        header["value"]
        for header in message_detail["payload"]["headers"]
        if header["name"] == "Subject"
      ][0]
      # from
      message["from"] = [
        header["value"]
        for header in message_detail["payload"]["headers"]
        if header["name"] == "From"
      ][0]
      messages.append(message)
    return messages

  except errors.HttpError as error:
    print(f"An error occurred: {error}")

# customize 3
def get_code_with_regrex(message):
  content = message.replace('\n','').replace('\r','')
#  print(content)
  # 認証コード：123456
  pattern = r'認証コード：?([0-9]+)'
  result = re.search(pattern, content)
  if result:
  	return result.group(1)
  else:
  	return "no hit"

```

こちらのコードは、アソビューで新規会員登録した時の認証コードを受けた時、そのコードを取得するライブラリである。
自分のテストに合わせた修正をする必要がある

| 改修か所 | 内容 |  
|:-----------|:------------|
|# customize 1 LIBRARY_DIR|T-DASHのdata pathを実行者に合わせたものにする|
|# customize 2 query|メールで検索するキーワードを指定する|
|# customize 3 get_code_with_regrex|メール本文から認証コードを取得する正規表現|

## カスタム動作

カスタム動作では、get_mfa_code_from_gmailを呼び出し、結果をT-DASHで使えるように変数${MFA_CODE}に入れる。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/267085/409b3d02-8ac6-3d65-b6e9-a5216d70bc83.png)

ライブラリの細かい設定では、pipで3つのgoogleライブラリをinstallする

```
pip install   google-api-python-client 
pip install   google-auth-httplib2 
pip install   google-auth-oauthlib
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/267085/09c140d1-52c0-7744-431b-fa59f4f2bf05.png)

## スクリプト

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/267085/81e765fd-ae4f-3249-e3a0-8695153697de.png)

流れ的には以下のようになる
1. メールに認証コードを飛ばす
2. メール受信まで待つ
3. メールから認証コードを取り出す（カスタム動作）
4. 認証コードを入力する


なお、初めてテスト実行すると、以下のようにブラウザでgoogleのloginが求められる。
続行して、対象のアカウントでログインする必要がある。
ここは手動で処理する必要がある。

※tokenが有効である限り、再度ログインは求められない。


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/267085/bdec5cde-1933-ba1c-9be6-24bc0d8d9e7a.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/267085/4b5bb622-3cbe-6e65-302b-b3cb192b197a.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/267085/c37071dd-617d-4fe0-b06e-e2597133f01e.png)