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の一部抜粋。
#import "NXOAuth2.h"

//for Feedly Oauth2(sandbox)
//account type
static NSString * const kOauth2ClientAccountType = @"Feedly";
//clientId
static NSString * const kOauth2ClientClientId = @"sandbox";
//Client Secret
static NSString * const kOauth2ClientClientSecret = @"CLIENTSECRET";
//Redirect Url
static NSString * const kOauth2ClientRedirectUrl = @"http://localhost";
//base url
static NSString * const kOauth2ClientBaseUrl = @"https://sandbox.feedly.com";
//auth url
static NSString * const kOauth2ClientAuthUrl = @"/v3/auth/auth";
//token url
static NSString * const kOauth2ClientTokenUrl = @"/v3/auth/token";
//scope url
static NSString * const kOauth2ClientScopeUrl = @"https://cloud.feedly.com/subscriptions";

+ (void)initialize {
    NSString *authUrl = [kOauth2ClientBaseUrl stringByAppendingString:kOauth2ClientAuthUrl];
    NSString *tokenUrl = [kOauth2ClientBaseUrl stringByAppendingString:kOauth2ClientTokenUrl];

    //setup oauth2client
    [[NXOAuth2AccountStore sharedStore] setClientID:kOauth2ClientClientId
                                             secret:kOauth2ClientClientSecret
                                              scope:[NSSet setWithObjects:kOauth2ClientScopeUrl, nil]
                                   authorizationURL:[NSURL URLWithString:authUrl]
                                           tokenURL:[NSURL URLWithString:tokenUrl]
                                        redirectURL:[NSURL URLWithString:kOauth2ClientRedirectUrl]
                                     forAccountType:kOauth2ClientAccountType];
}
次に、UIWebViewを設定したUIViewControllerにて認証の成功可否を通知してもらうための設定を行う。
NTAuthWebViewController.mの一部抜粋
定義してるヘッダとUIWebViewのアウトレット。
#import "NTAuthWebViewController.h"
#import "NXOAuth2.h"
#import "NTAppDelegate.h"

@interface NTAuthWebViewController ()
@property (weak, nonatomic) IBOutlet UIWebView *webView;
@property (strong, nonatomic) id successObserver;
@property (strong, nonatomic) id failObserver;
認証用の通知設定。それぞれ認証成功、失敗時に通知されるオブザーバーを登録している。 WebViewを閉じたら通知の必要はなくなるので、解除用にオブザーバーオブジェクトを退避している。 認証成功時はユーザプロファイル取得用のメソッドを呼び出している。OAuth2ClientのAccount情報にはユーザデータを 格納する領域が用意されており、ユーザプロファイル等のデータを格納してKeychainに保存することが出来る。 フロー図の(4)まで完了すれば、成功の通知が行われる。
- (void)p_addOauth2Notification {
    //setup notifications for success or fail
    //for success
    self.successObserver = [[NSNotificationCenter defaultCenter]
                            addObserverForName:NXOAuth2AccountStoreAccountsDidChangeNotification
                            object:[NXOAuth2AccountStore sharedStore]
                            queue:nil usingBlock:^(NSNotification *notification) {
                                NSLog(@"Success.");
                                
                                //get authinticate userinfo
                                NSDictionary *dict = notification.userInfo;
                                NXOAuth2Account *account = [dict valueForKey:NXOAuth2AccountStoreNewAccountUserInfoKey];
                                //get user profile
                                [self p_getUserProfile:account];
                                
                            }];
    
    //for fail
    self.failObserver = [[NSNotificationCenter defaultCenter]
                         addObserverForName:NXOAuth2AccountStoreDidFailToRequestAccessNotification
                         object:[NXOAuth2AccountStore sharedStore]
                         queue:nil
                         usingBlock:^(NSNotification *note) {
                             NSLog(@"Fail.");
                             
                             //pop navigation controller
                             [self.navigationController popViewControllerAnimated:YES];
                             
                         }];

}
フローの(1)の開始。認証を開始する。OAuth2Clientの方で準備されたNSURLが受け渡されるので、そのNSURLを使用して UIWebViewでWebアクセスを行う。 フローの(2)へ遷移し、認証用ページが表示されるので、ユーザ、パスワードを入力する。後はライブラリ側で フローの(3)、(4)を処理してくれて、成功した場合は通知が行われることになる。
- (void)p_startRequest {
    [[NXOAuth2AccountStore sharedStore] requestAccessToAccountWithType:kOauth2ClientAccountType
                                   withPreparedAuthorizationURLHandler:^(NSURL *preparedURL) {
                                       //start authentication request.
                                       [_webView loadRequest:[NSURLRequest requestWithURL:preparedURL]];
                                       
                                   }];
}
こんな感じで上記メソッドを呼び出している。
- (void)viewDidLoad
{
    [super viewDidLoad];
 // Do any additional setup after loading the view.

    _webView.delegate = self;
    
    //init notifications
    [self p_addOauth2Notification];
    [self p_startRequest];
    
}
ユーザプロファイル取得のコードと通知用オブザーバーの削除は以下。
- (void)p_getUserProfile:(NXOAuth2Account *)account {
    //get user profile on feedly user
    NSLog(@"account info : %@", account);
    
    NSURL *targetUrl = [NSURL URLWithString:@"https://sandbox.feedly.com/v3/profile"];
    [NXOAuth2Request performMethod:@"GET"
                        onResource:targetUrl
                   usingParameters:nil
                       withAccount:account
               sendProgressHandler:^(unsigned long long bytesSend, unsigned long long bytesTotal) {
                   //TODO
               }
                   responseHandler:^(NSURLResponse *response, NSData *responseData, NSError *error) {
                       NSLog(@"error : %@", error);
                       NSLog(@"response : %@", response);
                       NSString *jsonString = [[NSString alloc] initWithData:responseData encoding:NSUTF8StringEncoding];
                       NSLog(@"response data : %@", jsonString);
                       
                       //
                       if (!error) {
                           //success
                           NSLog(@"get profile success.");
                           //json変換してDictionary型をuserDataとして格納する
                           NSDictionary *dict = [NSJSONSerialization JSONObjectWithData:responseData options:kNilOptions error:nil];
                           if (dict) {
                               //set user data
                               [account setUserData:dict];
                           }
                           
                           //pop viewcontroller
                           [self.navigationController popViewControllerAnimated:YES];
                           
                       } else {
                           //error
                           NSLog(@"get profile failer.");
                       }
                       
                   }];
    
}

- (void)viewDidDisappear:(BOOL)animated {
    //hide network activity indicator
    [UIApplication sharedApplication].networkActivityIndicatorVisible = NO;
    //remove notifications
    [self p_removeOauth2Notification];
    
}

- (void) p_removeOauth2Notification {
    [[NSNotificationCenter defaultCenter] removeObserver:self.successObserver];
    [[NSNotificationCenter defaultCenter] removeObserver:self.failObserver];
}
自前のUIWebViewを利用しているので、フローの(3)リダイレクトのハンドリングは 自前コードで記載してライブラリ側へ受け渡す必要がある。 以下メソッドはUIWebVIewがHTTPリクエストを行う前に呼び出されるメソッドで、 このメソッドでリクエスト先URLをハンドリングしてリダイレクトURLと等しいか判定 する必要がある。リダイレクトURLと等しいかの判定処理はライブラリ側で行ってくれる。 その呼出が、
if ([[NXOAuth2AccountStore sharedStore] handleRedirectURL:[request URL]]) {
となる。
#pragma mark - UIWebViewDelegate

- (BOOL) webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType {
    if ([[NXOAuth2AccountStore sharedStore] handleRedirectURL:[request URL]]) {
        return NO;
    }
    return YES;
}
サンプルコードは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 件のコメント :

コメントを投稿