iOSアプリにおけるOAuth連携の実装


拙作のiPhoneアプリShareAlbum (http://bit.ly/sharealbum_jp) ではOAuthを使ってInstagramやTumblrなど様々なWebサービスと連携しています。このTipsではOAuth認可・認証のWebサービスと連携するアプリケーションの実装についてGoogleのライブラリによる説明を書いています。

![ShareAlbum](http://25.media.tumblr.com/37d45982f2fbd2cc879675fc80b68c5b/tumblr_midbtfWawv1qzzao7o1_400.png)

OAuth連携することでこんなアプリケーションも簡単に作れますよということです

# OAuth2.0について

まずはOAuth2.0についてiOSアプリ開発者が知るべきことは下記の通りです

* 基本的にAPIの認可のためにアクセストークンを取得する
* アクセストークンはAPIのパラメータにセットしてリクエストを行う
* アクセストークン取得までのフローは複数あり、主に認可コードフローやインプリシットグラントフローが使われる
* iOSからはUIWebViewを用いてログイン画面を表示する

![](http://24.media.tumblr.com/8e388cc756d2d5be70f2f27c9a2506d5/tumblr_midd004iY81qzzao7o1_400.jpg)

UIWebViewでログイン画面を出すとそのOAuthサービスのデザインのまま表示されます

## フローについて

* 認可コードフロー
 + 別名サーバサイドWebアプリケーションフロー、エクスプリシットグラントフローとも呼ばれます
* インプリシットグラントフロー
 + 別名クライアントサイドWebアプリケーションフローとも呼ばれます
* リソース所有者パスワードクレデンシャルフロー
* クライアントクレデンシャルフロー

次にこれらフローについて概要をまとめてみました

## 認可コードフローの概要
OAuthサービスとアプリケーションが共通のclient_secretを保持しておき、それを使って認可コードを取得、これとアクセストークンを交換します。
Webサービスの場合、ブラウザは認可コードについて保持することはありますが、アクセストークンを保持していないのでブラウザ履歴や,referer,jsなどからアクセスされるリスクが低くなります。リフレッシュトークンを使ってアクセストークンをリフレッシュすることが許されています。

#### 特徴まとめ
* 認可コードフローでは認可コードと引き換えにアクセストークンを手に入れる
* 認可コードはクエリパラメータとしてリダイレクトURLに付けられ送られてくる
* アクセストークンはそのリダイレクトURLにアクセスした際にレスポンスとして受け取る


#### 使われるケース
+ 長期に渡るアクセスやクライアントアプリがWebアプリケーションサーバの場合

## インプリシットグラントフローの概要
iPhoneやAndoridなどクライアントアプリケーションでclient_secretを秘匿できないケースのために考えられたフローです。認可コードが不要ですが、そのためアクセストークンの期限が切れた場合に、ID/パスワードによるフローをユーザーに行わせたいため、リフレッシュトークンを利用させることはOAuthの仕様上定義されていません。

#### 使われるケース
+ iPhone,AndroidやJS、Flashなどclient_secretを保持できない（リバースエンジニアリングされると見られる）アプリケーション

## リソース所有者パスワードクレデンシャルの概要
ユーザ名とパスワードを直接access_tokenと交換できます。APIプロバイダ自身（つまりサービス元）が開発したモバイルアプリケーションなど信用できるクライアントのみで使われます。

#### 使われるケース
+ OAuthサービス元自身が作成するモバイルアプリケーション


## クライアントクレデンシャルの概要
特定のユーザでなくストレージサービスやDBサービスのAPIアクセス方法です。

#### 使われるケース
+ Webサービスとストレージ系サービス間の認可がすでに委譲済みな場合


# OAuthサービスへのアプリケーション登録例
## InstagramのOAuthについて

Instagramはデベロッパー用の管理画面が有り、そこから利用するアプリ情報を登録します。
http://instagram.com/developer/clients/manage/

アプリケーションの新規登録画面は次の通り

![](http://25.media.tumblr.com/f1b960c1964d8a092f68d5e62035c513/tumblr_mib6cfePZu1qzzao7o1_1280.png)

それぞれ入力するのは次の項目になります

・アプリケーション名 (ユーザに表示される）
・アプリケーションについての詳細(ユーザに表示される）
・ウェブサイトURL(ユーザに表示される）
・リダイレクトURL(認可コードをレスポンスパラメータとして付与されるURL）

アプリケーション登録で重要なのは最後のリダイレクトURLです。

通常、リダイレクトURLはInstagramのアクセストークンを使いたいWebサービスのURLとします。
InstagramのID/パスワード入力画面からそのURLへクエリパラメータとして認可コードを付けてリダイレクトされWebサービスに戻ってくることで、OAuthの認可処理が完了しアクセストークンの利用ができるようになります。

しかしiOSアプリはWebアプリではないため、リダイレクトされるサイトはありません。iOSアプリ側はOAuthサーバーからのリダイレクト要求をキャンセルし、認可コードだけを取得するようにします。そのため、この連携アプリ登録時に入力するリダイレクトURLはダミーでも構いません。


# iOSアプリ側の仕様について

ID/パスワードの入力はUIWebViewを使うことができますが、UIWebView内の通信ではレスポンスとしてステータスコードが取得できません。

そのため、OAuthサーバーに登録したリダイレクトURLをあらかじめiOSアプリ側で保存しておき、リダイレクト処理が実行されようとする際に、リダイレクトされようとするURLとあらかじめ保存していたURLとを比較することによってUIWebViewのリダイレクトの通信をキャンセルし、必要があればNSURLConnectionで別途通信を行いレスポンスを取得します。

## 実装例

具体的な実装例はGoogleのOAuthライブラリであるGTMOAuth2ViewControllerTouch.mから、UIWebViewがOAuthサービス側からのリダイレクト命令をうけた際に、UIWebViewのdelegateであるwebView:shouldStartLoadWithRequest:navigationType:が実行されるため、リダイレクトしようとする際に処理を挟んでリダイレクトをキャンセルしているのがわかると思います


```objectivec:GTMOAuth2ViewControllerTouch.m
- (BOOL)webView:(UIWebView *)webView
  shouldStartLoadWithRequest:(NSURLRequest *)request
              navigationType:(UIWebViewNavigationType)navigationType {

  if (!hasDoneFinalRedirect_) {
	//まだアクセストークンを取得するためのリダイレクトがされていなければ
	//リクエストからリダイレクトさせるかを判定させてアクセストークンを取得させる
    hasDoneFinalRedirect_ = [signIn_ requestRedirectedToRequest:request];
    if (hasDoneFinalRedirect_) {
      //UIWebViewのリクエストをキャンセルさせる（この時点でアクセストークンは取得済み）
      return NO;
    }
  }
  return YES;
}
```

[signIn_ requestRedirectedToRequest:request];のrequestRedirectedToRquest:メソッドについて掘り下げましょう。この中では保持していたURLとリダイレクトされようとしたURLを比較しています。

```objectivec:GTMOAuth2SignIn.m
// リダイレクトURLとして指定したコールバックURLなら
// このメソッドはアクセストークンを取得しYESを返す
- (BOOL)requestRedirectedToRequest:(NSURLRequest *)redirectedRequest {
  
  //あらかじめ保持しているリダイレクトURLをNSStringとしてローカルに保持
  NSString *redirectURI = self.authentication.redirectURI;
  if (redirectURI == nil) return NO;

  //ここではGoogle特有のRedirectURIと同じかどうかをチェックしている。
  //今回の話とは関係ない
  NSString *standardURI = [[self class] nativeClientRedirectURI];
  if (standardURI != nil && [redirectURI isEqual:standardURI]) return NO;

  //あらかじめ保持していたリダイレクトURLをNSURLにしておく（比較のため）
  NSURL *redirectURL = [NSURL URLWithString:redirectURI];
  //リクエストしようとしたURLをNSURLにしておく（比較のため）
  NSURL *requestURL = [redirectedRequest URL];

  //リクエストしようとしたURLをhostとパスに分ける、分けて比較するのは
  //about:blankを指定した際に分ける必要があったから？
  NSString *requestHost = [requestURL host];
  NSString *requestPath = [requestURL path];

  //リクエストしようとしたURLが保持していたURLと同じならコールバックURLとしてYESにする
  BOOL isCallback;
  //
  if (requestHost && requestPath) {
    //ここで比較
    isCallback = [[redirectURL host] isEqual:[requestURL host]]
                 && [[redirectURL path] isEqual:[requestURL path]];
  } else if (requestURL) {
    // handle "about:blank"
    isCallback = [redirectURL isEqual:requestURL];
  } else {
    isCallback = NO;
  }

  if (!isCallback) {
    // tell the caller that this request is nothing interesting
    return NO;
  }

  // we've reached the callback URL

  // try to get the access code
  if (!self.hasHandledCallback) {
    //クエリパラメータをリクエストしようとしたオブジェクトから取得
    NSString *responseStr = [[redirectedRequest URL] query];
    //
    [self.authentication setKeysForResponseString:responseStr];

#if DEBUG
    NSAssert([self.authentication.code length] > 0
             || [self.authentication.errorString length] > 0,
             @"response lacks auth code or error");
#endif
    //アクセストークンを取得するリクエストを実行
    [self authCodeObtained];
  }
  // tell the delegate that we did handle this request
  return YES;
}

```

ちなみに、クエリにある認可コードはURLエンコードされているので、UTF8でデコードしてあげています

```objectivec:
+ (NSString *)unencodedOAuthParameterForString:(NSString *)str {
  NSString *plainStr = [str stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
  return plainStr;
}
```


# 最後に

OAuthの説明においてあやふやな部分や、間違っている箇所があればコメントもしくは編集リクエストなどでご指摘頂ければと思います

# 参考

デジタル・アイデンティティ技術最新動向（2）：RFCとなった「OAuth 2.0」――その要点は？ (1/2) - ＠IT
http://www.atmarkit.co.jp/ait/articles/1209/10/news105.html