この記事が伝えること
タイトルの内容をやろうとして想定外にハマったのですが、解決できたのでその時わかったことを共有します。
実際の実装の手順も詳しく載せました。
実際の手順は準備からどうぞ。
やりたかったこと
簡単なGmailクライアントアプリ=メーラー的なものを作りたいと思った。
OAuth2で、ユーザーの複数のGmailアカウントのデータへのアクセスを承認してもらいたい。
複数のアカウントというところが今回は重要でした。
メーラーなので、Gmailのアカウントを複数登録できるようにしたい。
想定外にハマったこと
公式のGmailAPIリファレンスだけを見ていても、上記の複数アカウントのOAuth認証が達成できなかった。
ハマったのはGmail APIというより、GoogleのOAuth認証の実行です。
結論から言うと、今のGoogle APIを使うためのOAuthライブラリは、Google/SignIn
と、GTMAppAuth
の2種類が存在し、この記事の目的で言うなら、後者の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 LibrariesのObjective-C samplesがGTMAppAuth
を使用しているので、僕はそこから辿り着きましたが、普通はどうやって見つけるんだろう...
"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を入力します。
で、↑赤い矢印が指すボタンをクリック。
↑赤い矢印が指すボタンをクリック。
↑赤い矢印が指すボタンをクリック。
そうすると"Download GoogleService-Info.plist"と出てきますが、今回これは使わないので、ダウンロードしなくて大丈夫です。
それから、その下でGoogle/SignIn
をPodfileに書き足すように書いてありますが、これも使わないので、書き足しません。
↑このページは何もせず"Enable the Gmail API"を押します。
↑そうするとこんな画面に飛ぶので、今さっき入力したApp名を選択し、続行を押します。
↑Gmail APIが有効になりました。認証情報に進むを押します。
↑使用する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 Paths
をYes
にすると読み込むようになりました。
OAuth認証
やっとここからコード書きます!
AppDelegate
まずはAppDelegateです。必要な部分だけ抜粋してます。
// 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
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するためのライブラリには、
GoogleSignIn
とGTMAppAuth
がある。 - 「Googleでログイン」したいのではなく、OAuthでアクセストークンが欲しいだけの場合は
GTMAppAuth
を使う。