2014年2月27日木曜日

iOSでOAuth2認証を行う(feedlyクライアントの作成)

The feedly Cloud APIが一般公開されていることはご存知の方は多いと思うが、APIを利用している方は そう多く無いのではないだろうか。 The feedly Cloud APIはsandboxという形式で開発者に開発用環境を提供している。 開発の序盤はこの開発環境を使用してAPIの使用方法やアプリの動作確認を行い、 その後リリースする準備が出来たら本環境で利用可能なクライアントIDを払い出してもらう という流れになると思う。(本環境に対応したクライアントIDの払い出しは別途Emailで申請する必要があるようだ)
iOSでのOAuth2認証
The feedly Cloud APIを使用するにはまずOAuth2認証が必要だ。これが一番厄介で(昔よりは良くなっている) 認証さえ済ませてしまえば、後はAPIを呼び出して必要な情報を取り出し、好きなように処理すれば良い。 iOS用のOAuth2ライブラリが存在するので、そのライブラリを利用してOAuth2認証を実装してみようと思う。
今回の目標;The Feedly Cloud(Sandbox)にOAuth2認証して認証アカウント一覧を表示する。
FeedlyにOAuth2認証して、プロファイル取得APIを呼び出してアカウントプロファイル情報の取得を行う。 認証したユーザを一覧表示する機能と認証済みアカウントの削除(サインアウト)を行う機能を実装する。
OAuth2ライブラリ
さてさて、iOSで利用可能なOAuth2ライブラリはいくつか存在する。以下2つのライブラリを試してみた。 gtm-oauth2はGoogleが提供しているライブラリで、少し試してみたが、gtm-oauth2では 画面(UIViewController)が提供されており、UIWebViewの実装も提供されている。 やや、Google認証に特化したコードやUIがiOS7対応されておらず結局自分で修正する必要があった。 一方、OAuth2Clientは画面の提供までは無いか、その分汎用性が高く 自前でUIWebViewを実装して組み込む事が可能な作りになっている。 また、認証データはKeychainに格納され、アクセストークンが無効になった場合はリフレッシュトークンから アクセストークンの再取得も行ってくれると思う。 ということで、今回はOAuth2Clientを利用させてもらい、OAuth2認証 を試してみたい。
Feedly Cloud Developer Programへのサインアップ(現在は必要無し)
昔はThe feedly Clound APIを利用するために、サインアップ用フォームに必要事項を入力し、 clientId, clientSecretを払いだしてもらっていたが、最近は 開発者用developer forum に提示されるように変更されているみたいだ。面倒くさくなったのかな。 因みに、払いだしたclientId, clientSecretには有効期限があり毎月1日にリセットされてしまう。
OAuth2Clientライブラリの取り込み
取り込みはCocoaPodsで一発。本当に便利だなぁ。もうCocoaPods無しの生活には戻れないほど便利。 本家のGitHubからCocoaPodsで取り込めば良いと思うが、後述する理由により改善したバージョンを GitHubに上げているので、用途に応じてどちらを取り込むか決めて欲しい。 CocoaPodsでの取り込み方法も含め、後述する。
OAuth2認証
OAuth2の認証フローについて整理すると以下のイメージになる。と思う。 このイメージにそって説明していきたいと思う。
認証ページを表示するUIWebViewの作成
StoryBoardでアカウント一覧を表示するUIViewControllerと認証Webページを表示するUIViewControllerを作成する。 以下のイメージで作成しているが、認証用のWebViewを配置していれば他は重要ではないので、任意で作成してもらって構わない。
OAuth2Clientの初期化と初期設定
初期化するのにベストな箇所はOAuth2ClientのREADMEにも書かれている通り次の箇所がベストだろう。
+[UIApplicationDelegate initialize]
ん?ってどこやねん。って思ったが、AppDelegate.mのこと。AppDelegateはUIApplicationDelegateプロトコル を採用しているクラスなので、この箇所はアプリケーション起動時に初期化されることになると思う。 以下サンプルコードのクライアントシークレットは実際のものに差し替えて欲しい。 また、ClientIdとRedirectUrlはFeedly Cloud Ssndbox用のものになる。本来は公開するものではないが、 開発用で一般に公開されているものなので、特に伏せ字にはしていない。
NTAppDelegate.mの一部抜粋。
  1. #import "NXOAuth2.h"
  2.  
  3. //for Feedly Oauth2(sandbox)
  4. //account type
  5. static NSString * const kOauth2ClientAccountType = @"Feedly";
  6. //clientId
  7. static NSString * const kOauth2ClientClientId = @"sandbox";
  8. //Client Secret
  9. static NSString * const kOauth2ClientClientSecret = @"CLIENTSECRET";
  10. //Redirect Url
  11. static NSString * const kOauth2ClientRedirectUrl = @"http://localhost";
  12. //base url
  13. static NSString * const kOauth2ClientBaseUrl = @"https://sandbox.feedly.com";
  14. //auth url
  15. static NSString * const kOauth2ClientAuthUrl = @"/v3/auth/auth";
  16. //token url
  17. static NSString * const kOauth2ClientTokenUrl = @"/v3/auth/token";
  18. //scope url
  19. static NSString * const kOauth2ClientScopeUrl = @"https://cloud.feedly.com/subscriptions";
  20.  
  21. + (void)initialize {
  22. NSString *authUrl = [kOauth2ClientBaseUrl stringByAppendingString:kOauth2ClientAuthUrl];
  23. NSString *tokenUrl = [kOauth2ClientBaseUrl stringByAppendingString:kOauth2ClientTokenUrl];
  24.  
  25. //setup oauth2client
  26. [[NXOAuth2AccountStore sharedStore] setClientID:kOauth2ClientClientId
  27. secret:kOauth2ClientClientSecret
  28. scope:[NSSet setWithObjects:kOauth2ClientScopeUrl, nil]
  29. authorizationURL:[NSURL URLWithString:authUrl]
  30. tokenURL:[NSURL URLWithString:tokenUrl]
  31. redirectURL:[NSURL URLWithString:kOauth2ClientRedirectUrl]
  32. forAccountType:kOauth2ClientAccountType];
  33. }
次に、UIWebViewを設定したUIViewControllerにて認証の成功可否を通知してもらうための設定を行う。
NTAuthWebViewController.mの一部抜粋
定義してるヘッダとUIWebViewのアウトレット。
  1. #import "NTAuthWebViewController.h"
  2. #import "NXOAuth2.h"
  3. #import "NTAppDelegate.h"
  4.  
  5. @interface NTAuthWebViewController ()
  6. @property (weak, nonatomic) IBOutlet UIWebView *webView;
  7. @property (strong, nonatomic) id successObserver;
  8. @property (strong, nonatomic) id failObserver;
認証用の通知設定。それぞれ認証成功、失敗時に通知されるオブザーバーを登録している。 WebViewを閉じたら通知の必要はなくなるので、解除用にオブザーバーオブジェクトを退避している。 認証成功時はユーザプロファイル取得用のメソッドを呼び出している。OAuth2ClientのAccount情報にはユーザデータを 格納する領域が用意されており、ユーザプロファイル等のデータを格納してKeychainに保存することが出来る。 フロー図の(4)まで完了すれば、成功の通知が行われる。
  1. - (void)p_addOauth2Notification {
  2. //setup notifications for success or fail
  3. //for success
  4. self.successObserver = [[NSNotificationCenter defaultCenter]
  5. addObserverForName:NXOAuth2AccountStoreAccountsDidChangeNotification
  6. object:[NXOAuth2AccountStore sharedStore]
  7. queue:nil usingBlock:^(NSNotification *notification) {
  8. NSLog(@"Success.");
  9. //get authinticate userinfo
  10. NSDictionary *dict = notification.userInfo;
  11. NXOAuth2Account *account = [dict valueForKey:NXOAuth2AccountStoreNewAccountUserInfoKey];
  12. //get user profile
  13. [self p_getUserProfile:account];
  14. }];
  15. //for fail
  16. self.failObserver = [[NSNotificationCenter defaultCenter]
  17. addObserverForName:NXOAuth2AccountStoreDidFailToRequestAccessNotification
  18. object:[NXOAuth2AccountStore sharedStore]
  19. queue:nil
  20. usingBlock:^(NSNotification *note) {
  21. NSLog(@"Fail.");
  22. //pop navigation controller
  23. [self.navigationController popViewControllerAnimated:YES];
  24. }];
  25.  
  26. }
フローの(1)の開始。認証を開始する。OAuth2Clientの方で準備されたNSURLが受け渡されるので、そのNSURLを使用して UIWebViewでWebアクセスを行う。 フローの(2)へ遷移し、認証用ページが表示されるので、ユーザ、パスワードを入力する。後はライブラリ側で フローの(3)、(4)を処理してくれて、成功した場合は通知が行われることになる。
  1. - (void)p_startRequest {
  2. [[NXOAuth2AccountStore sharedStore] requestAccessToAccountWithType:kOauth2ClientAccountType
  3. withPreparedAuthorizationURLHandler:^(NSURL *preparedURL) {
  4. //start authentication request.
  5. [_webView loadRequest:[NSURLRequest requestWithURL:preparedURL]];
  6. }];
  7. }
こんな感じで上記メソッドを呼び出している。
  1. - (void)viewDidLoad
  2. {
  3. [super viewDidLoad];
  4. // Do any additional setup after loading the view.
  5.  
  6. _webView.delegate = self;
  7. //init notifications
  8. [self p_addOauth2Notification];
  9. [self p_startRequest];
  10. }
ユーザプロファイル取得のコードと通知用オブザーバーの削除は以下。
  1. - (void)p_getUserProfile:(NXOAuth2Account *)account {
  2. //get user profile on feedly user
  3. NSLog(@"account info : %@", account);
  4. NSURL *targetUrl = [NSURL URLWithString:@"https://sandbox.feedly.com/v3/profile"];
  5. [NXOAuth2Request performMethod:@"GET"
  6. onResource:targetUrl
  7. usingParameters:nil
  8. withAccount:account
  9. sendProgressHandler:^(unsigned long long bytesSend, unsigned long long bytesTotal) {
  10. //TODO
  11. }
  12. responseHandler:^(NSURLResponse *response, NSData *responseData, NSError *error) {
  13. NSLog(@"error : %@", error);
  14. NSLog(@"response : %@", response);
  15. NSString *jsonString = [[NSString alloc] initWithData:responseData encoding:NSUTF8StringEncoding];
  16. NSLog(@"response data : %@", jsonString);
  17. //
  18. if (!error) {
  19. //success
  20. NSLog(@"get profile success.");
  21. //json変換してDictionary型をuserDataとして格納する
  22. NSDictionary *dict = [NSJSONSerialization JSONObjectWithData:responseData options:kNilOptions error:nil];
  23. if (dict) {
  24. //set user data
  25. [account setUserData:dict];
  26. }
  27. //pop viewcontroller
  28. [self.navigationController popViewControllerAnimated:YES];
  29. } else {
  30. //error
  31. NSLog(@"get profile failer.");
  32. }
  33. }];
  34. }
  35.  
  36. - (void)viewDidDisappear:(BOOL)animated {
  37. //hide network activity indicator
  38. [UIApplication sharedApplication].networkActivityIndicatorVisible = NO;
  39. //remove notifications
  40. [self p_removeOauth2Notification];
  41. }
  42.  
  43. - (void) p_removeOauth2Notification {
  44. [[NSNotificationCenter defaultCenter] removeObserver:self.successObserver];
  45. [[NSNotificationCenter defaultCenter] removeObserver:self.failObserver];
  46. }
自前のUIWebViewを利用しているので、フローの(3)リダイレクトのハンドリングは 自前コードで記載してライブラリ側へ受け渡す必要がある。 以下メソッドはUIWebVIewがHTTPリクエストを行う前に呼び出されるメソッドで、 このメソッドでリクエスト先URLをハンドリングしてリダイレクトURLと等しいか判定 する必要がある。リダイレクトURLと等しいかの判定処理はライブラリ側で行ってくれる。 その呼出が、
if ([[NXOAuth2AccountStore sharedStore] handleRedirectURL:[request URL]]) {
となる。
  1. #pragma mark - UIWebViewDelegate
  2.  
  3. - (BOOL) webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType {
  4. if ([[NXOAuth2AccountStore sharedStore] handleRedirectURL:[request URL]]) {
  5. return NO;
  6. }
  7. return YES;
  8. }
サンプルコードはGitHubにあげてあるので、省略した部分やアカウント一覧を表示している部分等は適宜GitHubの方を 参照して欲しい。 以下、実行画面。
The Feedly Cloud(sandbox)利用時のOAuth2Clientライブラリの問題
実は本家OAuth2Clientのままだと認証に成功することはなかった。 その原因はリダイレクトURLにあった。 Feedly(sandbox)の場合はリダイレクトURLが基本的に
http://localhost
となる。リダイレクトはフローの(3)でリダイレクトURLに認可コードがURLパラメータに付与されて通知されるのだが、
http://localhost/?code=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
のように通知される。このURLをリダイレクトしているのだが、「?」よりも前をリダイレクトURLとして 扱ってしまっている。
http://localhost/?code=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
これは間違っていないと思うが、リダイレクトURLがルートパスで終わってしまっている場合、http://localhostの後に「/」が サーバ側で付与されてしまい、結果的に設定しているリダイレクトURLと異なることとなり、リダイレクトのハンドリング に失敗してしまう。
http://localhost/
そこで、リダイレクトURLは初期化で設定してある値を受け渡すように修正した。 Feedly(sandbox)ではリダイレクトURLが固定(http://loaclhost以外にも定義されてはいるが)のため本改造を行った。 リダイレクトURLがルートパス以外であればこのような問題は発生していないと考えられる。
OAuth2Clientのインストール
CocoaPodsで。 上記で述べたリダイレクトURLの問題改善版を入れたい場合は、私が本家をForkして改善したものをGitHubに 登録してあるので、以下のようにPodfileを記載すればよい。
platform :ios, '6.0'
pod 'NXOAuth2Client', :git => 'https://github.com/takuran/OAuth2Client.git'
なんか怪しいので、本家をいれて試してみたいという人は以下のようにPodfileを記載すればよい。
platform :ios, '5.0'
pod 'NXOAuth2Client', '~> 1.2.5'
Podfileを作成したら以下コマンドでインストール完了。
#事前にpodのインストールはする必要あり。以下参照。
http://cocoapods.org/
% pod install

0 件のコメント :

コメントを投稿