Takahiro Octopress Blog

-1から始める情弱プログラミング

OAuth2.0認証の処理を自作しよう!

OAuth2.0認証処理を自分で作ろう! Objective-C編

本日は、gtm-oauthOAuth2Clientを使わずにOAuth2.0認証処理を自作してみます。

OAuth2.0認証を通すためのGoogle設定はiOSでGoogle OAuth認証がしたいを参照ください。
筆者が開発したライブラリは単純な下記処理のみ含んでいます。

  • OAuth2.0認証に必要な各種パラメータをKeychain Servicesに保存
  • アクセストークンを取得する処理
  • リフレッシュトークンから新規アクセストークンを取得する処理

ここで、Keychain Servicesを使うにあたって、LUKeychainAccessを利用しています。
これにより、複雑なKeychain ServicesをNSUserDefaultsの感覚で利用することができます。
また、Googleへの問い合わせなどのHTTP/HTTPSリクエストにAFNetworkingを利用しています。

OAuth2.0認証ライブラリの作成

下記にソースをそのまま載せておきます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
// OAuth2Client.m
#import "OAuth2Client.h"
#import "LUKeychainAccess.h"
#import "AFNetworking.h"

static NSString *callback =  @"http://localhost";
static NSString *visibleactions = @"http://schemas.google.com/AddActivity";

@implementation OAuth2Client

// シングルトンのインスタンス取得
+ (OAuth2Client *)sharedInstance {
  static OAuth2Client* sharedInstance = nil;
  static dispatch_once_t onceToken;
  dispatch_once(&onceToken, ^{
      sharedInstance = [OAuth2Client alloc] init];]
  });

  return sharedInstance;
}

// OAuth2.0認証に必要なパラメータを設定する処理
- (void)setUpOAuth2AccountClientId:(NSString *)clientId clientSecret:(NSString *)clientSecret scope:(NSString *)scope authorizationURL:(NSString *)authorizationURL tokenURL:(NSString *)tokenURL {
  [[LUKeychainAccess standardKeychainAccess] setObject:clientId forKey:@"clientId"];
  [[LUKeychainAccess standardKeychainAccess] setObject:clientSecret forKey:@"clientSecret"];
  [[LUKeychainAccess standardKeychainAccess] setObject:scope forKey:@"scope"];
  [[LUKeychainAccess standardKeychainAccess] setObject:authorizationURL forKey:@"authorizationURL"];
  [[LUKeychainAccess standardKeychainAccess] setObject:tokenURL forKey:@"tokenURL"];
}

// OAuth2.0認証に必要なリクエストを生成する処理
- (void)requestAccessToAccount:(void (^)(NSURL *preparedURL))withPreparedAuthorizationURLHandler {
  NSString *clientId = [[LUKeychainAccess standardKeychainAccess] objectForKey:@"clientId"];
  NSString *scope = [[LUKeychainAccess standardKeychainAccess] objectForKey:@"scope"];
  NSString *authorizationURL = [[LUKeychainAccess standardKeychainAccess] objectForKey:@"authorizationURL"];

  NSString *url = [[NSString stringWithFormat:@"%@?response_type=code&client_id=%@&redirect_uri=%@&scope=%@&data-requestvisibleactions=%@", authorizationURL, clientId, callback, scope, visibleactions];
  withPreparedAuthorizationURLHandler([NSURL URLWithString:url]);
}

// OAuth2.0認証のリダイレクトURIの一致の有無を確認する処理
- (BOOL)checkRedirectURI:(NSURLRequest *)request
{
  // HOSTの取得
  NSString *host = [[request URL] host];
  if ([host isEqualToString:@"localhost"]) {
      return YES;
  } else {
      return NO;
  }
}

// アクセストークンを取得する処理
- (void)getAccessToken:(NSURLRequest *)request completionHandler:(void (^)(NSString *accessToken))completionHandler {
  NSString *host = [[request URL] host];
  if ([host isEqualToString:@"localhost"]) {
      NSString* verifier = nil;
      NSArray* urlParams = [[request URL] query] componentsSeparatedByString:@"&"];
      for (NSString* param in urlParams) {
          NSArray* keyValue = [param componentsSeparatedByString:@"="];
          NSString* key = [keyValue objectAtIndex:0];
          if ([key isEqualToString:@"code"]) {
              verifier = [keyValue objectAtIndex:1];
              break;
          }
      }

      if (verifier) {
          NSString *clientId = [[LUKeychainAccess standardKeychainAccess] objectForKey:@"clientId"];
          NSString *clientSecret = [[LUKeychainAccess standardKeychainAccess] objectForKey:@"clientSecret"];
          NSString *tokenURL = [[LUKeychainAccess standardKeychainAccess] objectForKey:@"tokenURL"];

          // AFHTTPSessionManagerをインスタンス化
          AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
          // サーバエラー時のContent-Typeにtext/plainを許可(成功時にapplication/jsonが必要なので共に追加)
          manager.responseSerializer.acceptableContentTypes = [NSSet setWithObjects:@"text/plain", @"application/json", nil];
          // パラメータの設定
          NSDictionary *data = @{@"code": verifier, @"client_id": clientId, @"client_secret": clientSecret, @"redirect_uri": callback, @"grant_type": @"authorization_code"};

          [manager POST:tokenURL parameters:data success:^(NSURLSessionDataTask *task, id responseObject) {
              // 成功した場合
              if(responseObject && [responseObject count] > 0) {
                  NSString *accessToken = responseObject[@"access_token"];
                  NSString *refreshToken = responseObject[@"refresh_token"];
                  [[LUKeychainAccess standardKeychainAccess] setObject:accessToken forKey:@"accessToken"];
                  [[LUKeychainAccess standardKeychainAccess] setObject:refreshToken forKey:@"refreshToken"];

                  // 処理が終了したときに実行(アクセストークンを返す)
                  completionHandler(accessToken);
              }
          } failure:^(NSURLSessionDataTask *task, NSError *error) {
              // 失敗した場合
              NSError *err;
              NSData *data = [error userInfo] objectForKey:@"com.alamofire.serialization.response.error.data"];
              if(data) {
                  // エラーの中身がある場合
                  [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingAllowFragments error:&err];
              }
              // 失敗を返す
              failure(err);
          }];
      }
  } else {
      // HOST名が一致しない場合
  }
}

// リフレッシュトークンから新しいアクセストークンを取得する処理
- (void)getRefreshAccessToken:(void (^)(NSString *accessToken))success failure:(void (^)(NSError *error))failure {
  NSString *clientId = [[LUKeychainAccess standardKeychainAccess] objectForKey:@"clientId"];
  NSString *tokenURL = [[LUKeychainAccess standardKeychainAccess] objectForKey:@"tokenURL"];
  NSString *refreshToken = [[LUKeychainAccess standardKeychainAccess] objectForKey:@"refreshToken"];

  if(clientId && tokenURL && refreshToken) {
      // 必須パラメータがある場合
      // AFHTTPSessionManagerをインスタンス化
      AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
      // サーバエラー時のContent-Typeにtext/plainを許可(成功時にapplication/jsonが必要なので共に追加)
      manager.responseSerializer.acceptableContentTypes = [NSSet setWithObjects:@"text/plain", @"application/json", nil];

      // パラメータの設定
      NSDictionary *data = @{@"client_id": clientId, @"refresh_token": refreshToken, @"grant_type": @"refresh_token"};

      [manager POST:tokenURL parameters:data success:^(NSURLSessionDataTask *task, id responseObject) {
          // 成功した場合
          if(responseObject && [responseObject count] > 0) {
              NSString *accessToken = responseObject[@"access_token"];
              NSString *refreshToken = responseObject[@"refresh_token"];
              [[LUKeychainAccess standardKeychainAccess] setObject:accessToken forKey:@"accessToken"];
              [[LUKeychainAccess standardKeychainAccess] setObject:refreshToken forKey:@"refreshToken"];
              
              // 処理が終了したときに実行(アクセストークンを返す)
              success(accessToken);
          }
      }, failure(NSURLSessionDataTask *task, NSError *error) {
          // 失敗した場合
          NSError *err;
          NSData *data = [error userInfo] objectForKey:@"com.alamofire.serialization.response.error.data"];
          if(data) {
              // エラーの中身がある場合
              [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingAllowFragments error:&err];
          }
          // 失敗を返す
          failure(err);
      }];
  } else {
      // 必須パラメータがない場合
      // TODO: エラーオブジェクトを生成して返す
      failure(nil);
  }
}

@end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// OAuth2Client.h
#import <Foundation/Foundation.h>

@interface OAuth2Client : NSObject

/// シングルトンのインスタンス取得
+ (OAuth2Client *)sharedInstance;

/**
 OAuth2.0認証に必要なパラメータを設定する処理
 @param clientId クライアントID
 @param clientSecret クライアントシークレット
 @param scope アクセス範囲
 @param authorizationURL OAuth2.0 認証先URL
 @param tokenURL OAuth2.0 トークン取得先URL
 */
- (void)setUpOAuth2AccountClientId:(NSString *)clientId clientSecret:(NSString *)clientSecret scope:(NSString *)scope authorizationURL:(NSString *)authorizationURL tokenURL:(NSString *)tokenURL;

/**
 OAuth2.0認証に必要なリクエストを生成する処理
 @param withPreparedAuthorizationURLHandler OAuth2.0認証に必要なリクエストを返すBlock構文
 */
- (void)requestAccessToAccount:(void (^)(NSURL *preparedURL))withPreparedAuthorizationURLHandler;

/**
 OAuth2.0認証のリダイレクトURIの一致の有無を確認する処理
 @param request リクエスト
 @return リダイレクトURIの一致の有無
 */
- (BOOL)checkRedirectURI:(NSURLRequest *)request;

/**
 アクセストークンを取得する処理
 @param request アクセストークンの取得に必要なリクエスト
 @param completionHandler アクセストークンの取得処理が完了したら実行される処理
 */
- (void)getAccessToken:(NSURLRequest *)request completionHandler:(void (^)(NSString *accessToken))completionHandler;

/**
 リフレッシュトークンから新しいアクセストークンを取得する処理
 @param success 処理が成功した場合に実行(返却データはアクセストークン)
 @param failure 処理が失敗した場合に実行(返却データはエラーオブジェクト)
 */
- (void)getRefreshAccessToken:(void (^)(NSString *accessToken))success failure:(void (^)(NSError *error))failure;

呼び出し側のソース

呼び出し側のソースを記載します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
// ViewController.m
#import "ViewController.h"
#import "OAuth2Client.h"

static NSString *const clientId = @"";
static NSString *const clientSecret = @"";
static NSString *const authorizationURL = @"https://accounts.google.com/o/oauth2/auth";
static NSString *const tokenURL = @"https://accounts.google.com/o/oauth2/token";
static NSString *const scope = @"https://www.googleapis.com/auth/plus.login+https://www.googleapis.com/auth/userinfo.email+https://www.googleapis.com/auth/calendar";

@interface ViewController ()<UIWebViewDelegate>

@property (weak, nonatomic) IBOutlet UIWebView *webView;
@property (strong, nonatomic) NSMutableData *receivedData;
@property (assign, nonatomic) BOOL *isLogin;

@end

@implementation ViewController

- (void)viewDidLoad {
  [super viewDidLoad];

  _webView.delegate = self;
  NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
  if([defaults boolForKey:@"isLogin"]) {
      // 既にOAuth2.0認証実行済みの場合
      [[OAuth2Client sharedInstance] getRefreshAccessToken:^(NSString *accessToken) {
          // 成功した場合
          NSLog(@"%@", accessToken);
      } failure:^(NSError *error) {
          // 失敗した場合
          NSLog(@"%@", error);
      }];
  } else {
      // 初めてOAuth2.0認証を実行する場合
      // OAuth2.0認証に必要な各種パラメータの設定
      [[OAuth2Client sharedInstance] setUpOAuth2AccountClientId:clientId clientSecret:clientSecret scope:scope authorizationURL:authorizationURL tokenURL:tokenURL];
      // OAuth2.0認証リクエスト
      [[OAuth2Client sharedInstance] requestAccessToAccount:^(NSURL *preparedURL) {
          // リクエスト
          [_webView loadRequest:[NSURLRequest requestWithURL: preparedURL]];
      }
  }
}

- (void)didReceiveMemoryWarning {
  [super didReceiveMemoryWarning];
}

- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType {

  if([OAuth2Client sharedInstance] checkRedirectURI:request]]) {
      [[OAuth2Client sharedInstance] getAccessToken:request completionHandler:^(NSString *accessToken){
          // 処理が終了したら呼び出される
          if(accessToken.length > 0) {
              NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
              [defaults setBool:YES forKey:@"isLogin"];
              [defaults synchronize];
          }
          [webView removeFromSuperview];
      }];

      return NO;
  }

  return YES;
}

@end

これで簡単ですが、OAuth2.0認証の自作ライブラリの開発が完了です。
必要最低限の機能ですが、十分だと思います。
(もう少し時間をかけてより良いものを作りたいと思います笑。)

参考:

Comments