Автризация twitter или как убить сутки с библиотекой STTwitter

в 0:28, , рубрики: разработка под iOS

Предисловие

Убить сутки — реально, особенно учитывая наикорявейшую авторизацию twitter по сравнению например с тем же facebook.
Для сравнения алгоритм получения авторизации:

twitter
Получает token -> Использую этот токен заходим на страницу логина -> Ждем авторизации пользователя и получаем новый токен по callback_url -> обмениваем токен на токен сикрет.

facebook
Открываем facebook страницу логина с параметром calback_url и другими опциями -> ждем авторизиции а на callback получаем уже токен сикрет который можно использовать.

Да не спорю, есть SDK от обоих соц сетей, есть встроенные записи, но мне необходимо было реализовать именно данный метод авторизации. Потому что другие уже достаточно красиво реализованы и есть множество библиотек. Но что делать если встроенные записи не активированы, или были деактивированны. Не заставлять же пользователя лезь в настройки. Пока он будет ползать может забыть о приложении. Поэтому сел я за разработку логина через web для facebook и twitter.

Но поговорить я хотел именно о twitter, потому что как я сказал выше — twitter обладает просто колоссальным количеством подводных камней по сравнению с другими социалками предоставляющими oAuth1/2

И так погнали. Для разработки я решил использовать pod библиотеку STTwitter потому что я не хотел запорачиваться и писать шафраторы (ах да для oauth 1 если вдруг кто не знает нужно посылать контрольную сумму всех параметров заголовка запроса что делать мне лично было лень). Надо сказать на удивление хорошая бибилиотека, но документации для нее нети никакой (собственно поэтому и решил запостить свое решение). В остальном же- все стандартно.

Шаг 1 совместно со 2

Первым делом необходимо получить первый токен, который вообще не понятно зачем нужен. Ну да ладно, против системы не попрешь. Тут все просто, необходимо послать в post запросе авторизацию по данным приложения (api keys) и контрольные суммы.
Забегая немного вперед: используя полученный токен мы должны составить ссылку, по которой отправим пользователя. Чтобы не терять пользователя (не давать ему выходить из проложения) можно было бы сделать достаточно просто: открыть сафари, а затем по callback_url и кастомного url schemes для приложения (например myappscheme://parseresult) вернуться в приложение и заспарсить переданные твиттером параметры. И вот он первых подводный камень- twitter не дает зарегистрировать кастомные схемы url. Из-за этого возврат в приложение по такой схеме становится невозможным. Как быть? Прийдется делать кастомный UIWebView и простыл делегатным методом webView:shouldStartLoadWithRequest: перехватывать и проверять загружаемые ссылки нашего UIWebView. Теперь осталось сделать callback какойнибудь нереальный домен (который всеравно не загрузится, либо уже поменять на реальный, на всякий случай, когда приложение заимеет сайт) типа getmetothewounderlandandfeedmewitharainbownearpinkunicorns.com
Как только метод увидит что веб вью получила команду загрузить этот домен, мы завершаем его работц и парсим параметры на предмет токена.
У меня вся логика по обращению к библиотеке STT вынесена в отдельные классы, думаю будет понятно что где поэтому акцентировать что откуда не буду, темболее что пишу достаточно подробно.
Быблиотекой STTwitter это делается просто:

Сразу ленивая инициализация, чему я научился от бешенного старика из сенфорда :D (кто знает тот поймет). Вырезал всю лишнюю шелуху типа проверки на внутреннюю учетку в системе и тд.

// api и oauth - сильные публичные свойства класса twitterEngine
-(STTwitterOAuth *)oauth {
    if (!_oauth) {
        _oauth = [STTwitterOAuth twitterOAuthWithConsumerName:@"APP_NAME" consumerKey:TWI_API_KEY consumerSecret:TWI_API_SECREET];
    }
    return _oauth;
}

-(STTwitterAPI *)api {
    if (!_api) {
        if ([twitterEngine isAuthorized]) {
            NSLog(@"Full access account");
            _api = [STTwitterAPI twitterAPIWithOAuthConsumerKey:TWI_API_KEY consumerSecret:TWI_API_SECREET oauthToken:[[NSUserDefaults standardUserDefaults] valueForKey:TWI_STORE_AUTH_TOKEN] oauthTokenSecret:[[NSUserDefaults standardUserDefaults] valueForKey:TWI_STORE_AUTH_SECRET]];
        } else {
            NSLog(@"App only client");
            _api = [STTwitterAPI twitterAPIAppOnlyWithConsumerName:IN_APP_NAME consumerKey:TWI_API_KEY consumerSecret:TWI_API_SECREET];
        }
    }
    return _api;
}

Немного контроллера:

- (IBAction)loinTwitterButtonClicked:(id)sender {
    twitter = [twitterEngine new]; // twitterEngine мой класс для работы со всеми внешними средствами с twitter
    if ([twitterEngine isAuthorized]) {
        // handling authorized
    } else {
        twitter.delegate = self;
        [twitter sendRequestToken];
    }
}

Класса:

-(void)sendRequestToken
{
    [self.oauth postTokenRequest:^(NSURL *url, NSString *oauthToken) {
        [self.delegate openWebViewWithUrl:url]; // простейший протокол передачи полученного url для загрузки в UIWebVew
    } oauthCallback:@"http://getmetothewounderlandandfeedmewitharainbownearpinkunicorns.com" errorBlock:^(NSError *error) {
        // handling error
    }];
}

И снова контроллера (тут метод протокола, и делегат от UIWebView который перехватывает загружаемые URL

-(void) openWebViewWithUrl:(NSURL *)url
{
    // Если не задать размер за минусом статус бара браузер будет коряво накладываться на часы/батарейку/оператора
    UIWebView *browser = [[UIWebView alloc] initWithFrame:CGRectMake(0, 20, self.view.frame.size.width, self.view.frame.size.height - 20)];
    NSURLRequest *request = [NSURLRequest requestWithURL:url];
    [browser loadRequest:request];
    [browser setDelegate:self];
    [self.view addSubview:browser];
}

-(BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType {
    NSURL *url = [request URL];
    NSLog(@"Loading URL: %@", [url absoluteString]);
    if ([[url host] isEqualToString:@"getmetothewounderlandandfeedmewitharainbownearpinkunicorns.com"]) {
        [webView removeFromSuperview]; // Удаляем себя за не надобностью
        NSDictionary *params = [twitterEngine parseURLToParams:url];
        [twitter sendAccessToken:[params objectForKey:@"oauth_verifier"]]; // это и есть прямой выход на ШАГ 3 и обмен токена на сикрет.
        return NO; // если возвращаем NO то браузер просто не будет загружать эту ссылку, но параметры мы уже получили.
    }
    return YES;
}

Шаг 3

И походу последний. На изучение библиотеки STT на эту тему (последний шаг) я убил больше всего времени а решение оказалось в пару строчек. Но, как я и хотел я избежал главного, возни с контрольной суммой и соединения все в заголовки. Так же хочу заметить, что по тупости oAuth1 от twitter обмен токена oauth_verifier не посредством заголовка (!!!) как это делалось в предыдущих шагах, а именно а теле самого POST запроса. Вот тут STT мне воткнула палку в колесо. Как оказалось STT не имеет прямого инструмента работы через схему авторизации которую я выбрал, а именно не имеет метода обмена oauth_verifier на oauth_token_secret. Пришлось полезть глубже и использовать очень глубокие методы суперклассов, которые по сути были чуть ли не простым системным методом отсылки. Немного отступая — http протокол знаю, умею, но мне хватило в свое время повозиться с сокетами чтобы я возненавидел такую низкоуровневую работу с протоколами. Мне ближе если можно так сказать программирование средних уровней (не UI но и не прямые протоколы). Но пути назад уже небыло, и я уже приготовился писать свой дополнительный метод, который реализует обмен токена. Боже копаться в чужом коде, да еще и дописывать его- для меня это сущий кошмар, хотя бы потому, что его писал не я. Но деваться было некуда… как друг я заметил знакомую сроковую переменную @«oauth_verifier» и тут же начал изучать метод где она была замечена. Вообще, почему я не догадался воспользоваться поиском по этому ключу досих пор не знаю. Сэкономил бы несколько часов.
Что оказалось: есть в твиттере авторизация по pin коду. Схема похожая, но реализована она как раз для приложений которые не могу сделать callback внутрь и тем самым получить токен на обмен. По сути PIN в этой схеме есть упрощенный (ну блин, запомните с сайта 8 цифр и введете их в приложении?) токен для обмена. И так как я ни разу не изучал схему авторизации по пину (да и всеравно она задокументирована двумя абзацами даже в базе знаний твиттера) я не знал, что механизм у них один. Благо я решил назвать свой метод похожим на

postAccessTokenRequestWithPIN:successBlock:errorBlock:

Это тот самы метод библиотеки STT который по сути и реализововал то, что нужно было мне. Пару нажатий на клавиши и мы получаем последний метод класса twitterEngine реализующий обмен токена на токен сикрет и его запись в карман для последующего восстановления:

-(void)sendAccessToken:(NSString *)oauth_verifier
{
    [self.oauth postAccessTokenRequestWithPIN:oauth_verifier successBlock:^(NSString *oauthToken, NSString *oauthTokenSecret, NSString *userID, NSString *screenName) {
        [[NSUserDefaults standardUserDefaults] setObject:oauthToken forKey:TWI_STORE_AUTH_TOKEN];
        [[NSUserDefaults standardUserDefaults] setObject:oauthTokenSecret forKey:TWI_STORE_AUTH_SECRET];
        [[NSUserDefaults standardUserDefaults] synchronize];
    } errorBlock:^(NSError *error) {
        // handling error
    }];

Заключение

По сути все. Далее работа лежала над проверкой внутренней записи в телефоне что STT умеет делать по сути прямо при реализации. Если у вас есть какието мысли как улучшить данное решение будет интересно послушать. Тем более что по своей природе я как iOS разработчик склонен злоупотреблять протоколами и обзервингом, уж больно они мне нравятся.

Автор: ptuchster

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js