Help us understand the problem. What is going on with this article?

【iOS】Gmail APIを使って、ユーザーの複数のGmailアカウントからメールを取得する。

More than 3 years have passed since last update.

この記事が伝えること

タイトルの内容をやろうとして想定外にハマったのですが、解決できたのでその時わかったことを共有します。
実際の実装の手順も詳しく載せました。
実際の手順は準備からどうぞ。

やりたかったこと

簡単なGmailクライアントアプリ=メーラー的なものを作りたいと思った。

OAuth2で、ユーザーの複数のGmailアカウントのデータへのアクセスを承認してもらいたい。

複数のアカウントというところが今回は重要でした。
メーラーなので、Gmailのアカウントを複数登録できるようにしたい。

想定外にハマったこと

公式のGmailAPIリファレンスだけを見ていても、上記の複数アカウントのOAuth認証が達成できなかった。
ハマったのはGmail APIというより、GoogleのOAuth認証の実行です。

結論から言うと、今のGoogle APIを使うためのOAuthライブラリは、Google/SignInと、GTMAppAuth2種類が存在し、この記事の目的で言うなら、後者のGTMAppAuthを使う方が良いです。

前者のGoogle/SignInを使ってやろうとして一向にできず3日くらいの時間を消費しました。ツラい...😂

公式のAPIリファレンスにはSignInの方しか書いてないので、そもそもライブラリ自体が違うとか考えもしないわけで...

2017年6月現在、iOSアプリ開発において、Google APIを使うためのOAuthライブラリとして何を使うか

まず、"Gmail API"とググって出てくるこの公式のリファレンスGmail API | Google DevelopersにあるiOS Quick Startでは、ユーザーのOAuth2認証に、Google/SignInというライブラリを使用しています。

が、今回の目的においては、リファレンスに出てこないGTMAppAuthというもう一つの公式ライブラリを使いました。

GoogleSignInとGTMAppAuth

リファレンスに出てこないGTMAppAuthって何者なのよって話ですが、Google公式ブログの、APIについての重要な記事がこちら。

Google Developers Japan: ネイティブ アプリの OAuth インタラクションを最新にしてユーザビリティとセキュリティを向上する / 2016年9月9日金曜日

以下、上記記事より引用

この移行をサポートするため、皆様が利用できる最新のベスト プラクティスに基づくライブラリとサンプルを公開しています。

  • Google Sign-In: Android と iOS 向け。Google アカウントを使ったログインと OAuth の推奨 SDK です。
  • AppAuth: Android と iOS および OS X 向け。Google などの OAuth プロバイダで使用できるオープンソース OAuth クライアント ライブラリです。さらに、Objective-C 向け Google API クライアント ライブラリで AppAuth サポートを有効にするライブラリ GTMAppAuth(iOS および OS X 向け)と、GTM Session Fetcher プロジェクトも提供しています。

とのことです。

よくわかってなかったんですが、いわゆる「Googleでログイン」みたいなものを実装したいときはGoogle/SignIn(GIDSignIn)を使えば良くて、今回のようにただただOAuthでアクセストークンがほしい!みたいな場合はGTMAppAuthを使えば良いっぽいです。
間違ってたら教えてください...🙇

GTMAppAuthでググると英語の記事は結構出てきますね。
こんな記事(?)もありました。

Implementing OAuth 2.0 · MailCore/mailcore2 Wiki - Github
2017年1月ですが、GTMOAuth2は非推奨になったから、今後はGTMAppAuthを使うべきだよ!とのこと。
今まではGTMOAuth2っていうのを使っていたんですね。

ただでさえわかりにくい認証周りを英語で読むの大変です。ひえ〜
日本語が少ないので、この記事では日本語でお役に立てれば...

今回の目的における、GoogleSignIn(GIDSignIn)でできなかったこと

  • 複数アカウントでのAPIコール
    • APIを叩くクラスへ渡す認証情報が、サインインしたときしか取得できない
    • サインインは一度に1アカウントでしかできない。
  • そもそも認証情報が欲しいわけなのだけど、「サインイン」ってちょっと概念が違う...
    • セッションみたいに一時的にユーザーが承認されるのではなく、アクセストークンで半永久的に(ユーザーが連携を切るまで)承認してほしい。
  • 認証情報の楽なキーチェーンへの保存
    • 2度目以降にアプリを開いたときの自動ログインがなんか上手くいかない。

ググり力

Gmail API Client LibrariesObjective-C samplesGTMAppAuthを使用しているので、僕はそこから辿り着きましたが、普通はどうやって見つけるんだろう...

"Google OAuth"とかでググると最初に出てくるUsing OAuth 2.0 to Access Google APIs | Google Identity Platform | Google Developersの一番下にGTMAppAuthへのリンクがありますね。

うーん"Gmail API"でググっても出てこないわけだ。

GTMAppAuthについてはリファレンスページなどはないので、GithubのレポジトリのREADMEが2017年6月現在唯一(?)の公式の資料になります。

ググり力と英語力をもっとつけたい。
ヤバい、ポエムみたいになってきた。
次からGTMAppAuthを使った実際の手順 in Japaneseに入っていきます!

準備

Google Developersでの設定はできてるよ!って方はXcode側の準備からどうぞ!GoogleService-Info.plistは使いません。

Google Developers側の準備

まず、Google APIを使うには、Google DevelopersでAppを作り、APIを有効化する必要があります。

基本はiOS Quickstart | Gmail API | Google Developersの通りですが、今回はちょっとずつ違う箇所があるので説明していきます。

最初にXcodeで新しいプロジェクトを作り、Bundle IDを控えます。

Add Google Services | Google Developersにアクセスします。
そこで、好きなApp名を入力し、iOS Bundle IDには先ほど作成したプロジェクトのBundle IDを入力します。

スクリーンショット 2017-06-03 22.17.54.png

で、↑赤い矢印が指すボタンをクリック。

スクリーンショット 2017-06-03 22.21.39.png

↑赤い矢印が指すボタンをクリック。

スクリーンショット 2017-06-03 22.21.56.png

↑赤い矢印が指すボタンをクリック。

そうすると"Download GoogleService-Info.plist"と出てきますが、今回これは使わないので、ダウンロードしなくて大丈夫です。
それから、その下でGoogle/SignInをPodfileに書き足すように書いてありますが、これも使わないので、書き足しません。

スクリーンショット 2017-06-03 22.24.01.png

↑このページは何もせず"Enable the Gmail API"を押します。

スクリーンショット 2017-06-03 22.29.40.png

↑そうするとこんな画面に飛ぶので、今さっき入力したApp名を選択し、続行を押します。

スクリーンショット 2017-06-03 22.30.23.png

↑Gmail APIが有効になりました。認証情報に進むを押します。

スクリーンショット 2017-06-03 22.31.03.png

↑使用するAPIをGmail APIにし、APIを呼び出す場所をiOSにし、ユーザーデータにチェックを入れて次に進みます。

そうすると、「この目的に適した認証情報が既に存在します」と言われるので、「あ、はい」と呟いて「完了」を押します。

ごめんなさいこの認証情報の操作いらないですね笑
APIを有効化すると自動で作られるみたいです。

これでGoogle側の設定は終了です!
OAuth 2.0 クライアント IDという欄に「iOS client for {Bundle ID} (auto created by Google Service) 」という認証情報ができていればオッケーです!

あとでこのページの情報を使うので、ブラウザのウィンドウは開いたままで進んでください。

Xcode側の準備

ライブラリのインストール

ライブラリのインストールにCocoaPodsを使います。
Podfileの中身は以下の感じで。

platform :ios, '8.0'
use_frameworks!
target 'TestApp' do
    pod 'GoogleAPIClientForREST/Gmail', '~> 1.3.0'
    pod 'GTMAppAuth'
end

targetのプロジェクト名は自分のプロジェクトに変更してくださいね。
GoogleAPIClientForRESTの2017年6月現在の最新バージョンは1.3.0です。

リファレンスと違うのは

    pod 'GTMAppAuth'

の部分です。

pod installして、TestApp.xcworkspaceを開きます。

ライブラリの中身はObjective-Cですが、pod経由の場合はBridging-Headerなどは不要です。

URL Typesの追加

ユーザーにOAuth2でこのアプリを承認してもらう手順の中でリダイレクトが行われるので、公式にのっとってURL Typeを追加します。

まず、先ほど開いたままにしたGoogle APIsの認証情報ページから、先ほど作成した認証情報の詳細ページに飛びます(名前をクリック)。

そして、「iOS の URL スキーム」欄にある値をコピーします。

Xcodeに戻り、InfoのURL Typesにて+を押し、URL Schemeの欄に値をペーストします。
他の値は空のままでOKです。

実装

GTMAppAuthのREADMEに書いてある通りのことを、Swiftに置き換えてやっていきます。

GTMAppAuthが読み込まれない問題

やっと書くぜ!と思ったらXcodeがGTMAppAuthを認識してくれない...

[XCode]追加してインポートしたヘッダーファイルが見つからないときの対処法3つ

ということで僕の場合はAlways Search User PathsYesにすると読み込むようになりました。

OAuth認証

やっとここからコード書きます!

AppDelegate

まずはAppDelegateです。必要な部分だけ抜粋してます。

AppDelegate.swift
// AppAuthのimportを忘れずに!
import AppAuth
import UIKit
import CoreData

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?
    // メンバ変数を宣言します。
    var currentAuthorizationFlow: OIDAuthorizationFlowSession?

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {

        return true
    }

    // 〜〜〜〜〜〜〜〜〜〜〜〜
    // 〜メソッドの記述を省略〜
    // 〜〜〜〜〜〜〜〜〜〜〜〜

    // 以下のメソッドを書き足します。
    // 上で追加したURL Schemeでのリダイレクトを受けた際の処理です
    @available(iOS 9.0, *)
    func application(_ app: UIApplication, open url: URL, options: [UIApplicationOpenURLOptionsKey : Any] = [:]) -> Bool {
        // Sends the URL to the current authorization flow (if any) which will
        // process it if it relates to an authorization response.
        if (currentAuthorizationFlow?.resumeAuthorizationFlow(with: url))! {
            currentAuthorizationFlow = nil
            return true
        }
        // Your additional URL handling (if any) goes here.
        return false
    }

}


変更点はimport AppAuthと、currentAuthorizationFlowの宣言と、一番下のメソッドの実装の3点です。

GoogleのOAuth APIを叩く

次に、実際にGoogleのOAuth APIを叩きます。
Githubの例( GTMAppAuth/Example-iOS/)ではViewControllerに書いていますが、僕はViewControllerから処理を切り離すために、GoogleOAuth処理用のクラスを別に作りました。
ので、以降の処理はViewControllerに書いてもいいですし、別の場所に書いてもOKです。

まずは定数と変数宣言から。

private let scopes        = [kGTLRAuthScopeGmailModify]
private let kClientID     = クライアントID
private let kRedirectURL  = URL.init(string: "リバースクライアントID:/oauthredirect")
private let configuration = GTMAppAuthFetcherAuthorization.configurationForGoogle()
private var authorization: GTMAppAuthFetcherAuthorization?

scope

スコープというのは、このアプリがユーザーに要求する、ユーザー情報へのアクセス権限のことです。
Choose Auth Scopes | Gmail API | Google Developersから一覧を見ることができます。ユーザーに承認してもらえるよう、必要最小限の権限を選びましょう。

kClientID

先ほど上で作った認証情報の「クライアントID」のことです。
Google APIsの認証情報ページから自分のクライアントIDをコピペしましょう。
123456789-hogehoge.apps.googleusercontent.comのようなやつです。

kRedirectURL

リダイレクト先のURLです。先ほど上でURL Typesに追加したやつです。
Githubの例( GTMAppAuth/Example-iOS/Source/GTMAppAuthExampleViewController.m
46, 47行目)
では、
com.googleusercontent.apps.YOUR_CLIENT:/oauthredirect
のようになっているので、ここでも同じようにします。

Google APIsの認証情報ページの「iOSのURLスキーム」欄から値をコピーして、

URL.init(string: com.googleusercontent.apps.123456789-hogehoge:/oauthredirect)

となるようにペーストしましょう。

configuration

OAuth用にサービス(今回はGoogle)へ送るリクエスト(OIDAuthorizationRequest)を作る際に必要な設定です。
今回はGTMAppAuthがGoogle用の設定を用意してくれるので、GTMAppAuthFetcherAuthorization.configurationForGoogle()の戻り値を入れるだけです!

authorization

ユーザーが提示されたスコープを承認した際にGoogleから送られてくる認証情報オブジェクトが入ります。

次に、Googleへ送るリクエストを作ります。

let request = OIDAuthorizationRequest.init(configuration: configuration,
                                           clientId: kClientID,
                                           scopes: self.scopes,
                                           redirectURL: kRedirectURL!,
                                           responseType: OIDResponseTypeCode,
                                           additionalParameters: nil)

リクエストを送ります。

appDelegate.currentAuthorizationFlow = OIDAuthState.authState(byPresenting: request,
                                                              presenting: self.delegate as! UIViewController,
                                                              callback: { (authState, error) in
            if (authState != nil) {
                // 認証情報オブジェクトを生成
                self.authorization = GTMAppAuthFetcherAuthorization.init(authState: authState!)
                let accessToken = authState?.lastTokenResponse?.accessToken
                print("Authorization トークンを取得しました:\(String(describing: accessToken))")
        })

これでOAuth認証処理のコードは終わりです!
OIDAuthState.authState()が実行されると、ユーザーにOAuthで承認を要求する画面が表示されます。
コード自体は短いですね。
この辺はほとんどREADME通りなので、本家が見たければgoogle/GTMAppAuth
- Github
を見てください!

Gmail API叩く

やっと本命のAPIを叩くコードです。٩( ᐛ )و

で、GTMAppAuthのREADMEではGTMSessionFetcherServiceというクラスを使っているのですが、これに関してはiOS Quickstartで使われているGTLRServiceの方が便利です。
というのも、クエリの組み立てがこちらの方が分かりやすいし、レスポンスのJSONパースまで内部でやってくれて、専用のオブジェクトで返してくれるからです。

これまた、API専用のクラスを自分で作った方が使い勝手が良いと思います。
エッセンスだけ書いておきます!

例としてusers.messages.listを実行します。

import GoogleAPIClientForREST

let query = GTLRGmailQuery_UsersMessagesList.query(withUserId: userId)
let service = GTLRGmailService()
service.authorizer = GTMAppAuthFetcherAuthorization.init(fromKeychainForName: "NAME")
service.executeQuery( query,
                      delegate: self,
                      didFinish: #selector(callback) )

query

APIリクエストのクエリです。
いや、クエリですとか言って実際はリクエストのパスですね。
GTLRQueryクラスのサブクラスであるGTLRGmailQuery_HogeHogeのqueryメソッドを使うと簡単に生成してくれます。
API Reference | Gmail API | Google Developersを見ながら同じような単語を打ち込めばXcodeが補完してくれます!

ちなみにここでいうuserIDはメアドのことです。

service

実際にリクエストを送ってくれるオブジェクトです。
Gmailの場合はGTLRGmailServiceクラスのインスタンスです!

service.authorizer

認証情報オブジェクト(GTMAppAuthFetcherAuthorization)です。service.authorizerに正しい認証情報オブジェクトを入れることで、リクエストヘッダに自動的にアクセストークンを挿入してくれます。

ここでは、iOSのキーチェーンから、過去に保存した認証情報を取り出して使用しています。
認証情報の保存について後述します。

service.executeQuery

クエリ実行!
コールバックをセレクタで指定します。

コールバック

func callback( ticket : GTLRServiceTicket,
                         finishedWithObject response : GTLRGmail_ListMessagesResponse,
                         error : NSError? ){
        if let error = error {
            print("メッセージリストの取得に失敗しました。")
            print(error)
            return
        }
        print("メッセージリストの取得に成功しました。")
        print(response.messages!)
        print(response.nextPageToken!)
        print(response.resultSizeEstimate!)

    }

finishedWithObject

叩くAPIによってレスポンスの種類が違うので、finishedWithObjectの型もXcodeの補完に頼ります。
なんか網羅的に一覧にしてるのとかないの...

ま、というわけで、これでAPIを叩くことができた!!
わーい!!たーのしー!!今日はいいAPI日和だねー!!

認証情報の保存

で、次に気になるのが認証情報の保存についてです。

一度OAuthで認証してもらったら、あとは明示的な許可を必要としないでアプリを使ってほしい。そのためには、最初に得た認証情報をアプリに保存しておきたいわけです。

GTMAppAuthFetcherAuthorization

GTMAppAuthFetcherAuthorizationはOAuth認証のところで使用した認証情報のクラスですが、これがキーチェーンの保存までお世話してくれます!

// Serialize to Keychain
GTMAppAuthFetcherAuthorization.save(_authorization, toKeychainForName: kGTMAppAuthExampleAuthorizerKey)

// Deserialize from Keychain
let authorization = GTMAppAuthFetcherAuthorization.init(fromKeychainForName: kGTMAppAuthExampleAuthorizerKey)

// Remove from Keychain
GTMAppAuthFetcherAuthorization.removeFromKeychain(forName: kGTMAppAuthExampleAuthorizerKey)

簡単。
保存するときに指定したtoKeychainForName:さえ保存しておけば、いつでも復元可能です。
Google/SignIn(GIDSignIn)にはこういう機能が見当たらなかった...

認証情報を任意のキーで保存できるということは、アカウントごとに認証情報を保存するキーを保存しておけば、複数アカウントでのAPIコールが実現できるわけです!やったー!

複数にするときは、appDelegateに宣言した変数とかは場所を変えた方が良いかもですね。

おまけ:ユーザー情報を取得したい

GIDSignInを使うとサインインした段階で、そのユーザーの情報が返ってくるのですが、GTMAppAuthの場合はそういうのがなく...

2016年2月にGoogle People APIというのが発表されていたので、API練習も兼ねて使ってみました。

長くなりそうなので記事分けます。

【iOS】Google People APIでユーザーの情報を取得する

まとめ

  • GoogleのAPIを使うときにOAuthするためのライブラリには、GoogleSignInGTMAppAuthがある。
  • 「Googleでログイン」したいのではなく、OAuthでアクセストークンが欲しいだけの場合はGTMAppAuthを使う。
ryokkkke
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした