Pergunta

I have an iPad application which allows users to login to their Gmail account(s) using OAuth2. Thus far, the login process and email fetching is successful. However, when the app is closed and then re-opened after a (long) period of time, an error is produced "invalid credentials,' even though previous logins with the same credentials were successful.

Login Flow: 1) User logs in to gmail using OAuth 2. 2) User email address and oAuthToken provided by the GTMOAuth2Authentication object are saved to core data for future logins. 3) IMAP Session is created using saved email address and OAuthToken.

Here is the relevant code

Google Login

- (void)gmailOAuthLogin
{
  NSDictionary *googleSettings = [[EmailServicesInfo emailServicesInfoDict] objectForKey:Gmail];

  GTMOAuth2ViewControllerTouch *googleSignInController  = 
    [[GTMOAuth2ViewControllerTouch alloc] initWithScope:GmailScope clientID:GmailAppClientID clientSecret:GmailClientSecret keychainItemName:KeychainItemName completionHandler:^(GTMOAuth2ViewControllerTouch *googleSignInController, GTMOAuth2Authentication *auth, NSError *error){

   if (error != nil) {
        //handle error

   }   else {

    [[ModelManager sharedInstance] authenticateWithEmailAddress:[auth userEmail]   
      oAuthToken:[auth accessToken] imapHostname:[googleSettings objectForKey:IMAPHostName] imapPort:[[googleSettings objectForKey:IMAPPort]integerValue] smtpHostname:[googleSettings objectForKey:SMTPHostName] smtpPort:[[googleSettings objectForKey:SMTPPort]integerValue] type:EmailProtocolTypeImapAndSmtpGMail success:^(Account *account) {

       //create IMAP session using above arguments

        } failure:^(NSError *error) {
            //handle error                       
        }];

        }
    }];

      [self presentGoogleSignInController:googleSignInController];
    }

Create IMAP Session Using MailCore2

- (void)authenticateWithEmailAddress:(NSString *)emailAddress password:(NSString *)password oAuthToken:(NSString *)oAuthToken imapHostname:(NSString *)imapHostname imapPort:(NSInteger)imapPort smtpHostname:(NSString *)smtpHostname smtpPort:(NSInteger)smtpPort success:(void (^)())success failure:(void (^)(NSError *))failure
{
  self.imapSession = [[MCOIMAPSession alloc] init];
  self.imapSession.hostname = imapHostname;
  self.imapSession.port = imapPort;
  self.imapSession.username = emailAddress;
  self.imapSession.connectionType =  MCOConnectionTypeTLS;
  self.imapSession.password = nil;
  self.imapSession.OAuth2Token = oAuthToken;
  self.imapSession.authType = nil != oAuthToken ? MCOAuthTypeXOAuth2 :      
  self.imapSession.authType;

  [self.imapSession setConnectionLogger:^(void * connectionID, MCOConnectionLogType type,    
NSData * data){
NSLog(@"MCOIMAPSession: [%i] %@", type, [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]);
  }];

  self.smtpSession = [[MCOSMTPSession alloc] init];
  self.smtpSession.hostname = smtpHostname;
  self.smtpSession.port = smtpPort;
  self.smtpSession.username = emailAddress;
  self.smtpSession.connectionType = MCOConnectionTypeTLS;
  self.smtpSession.password = nil;
  self.smtpSession.OAuth2Token = oAuthToken;
  self.smtpSession.authType = nil != oAuthToken ? MCOAuthTypeXOAuth2 :   
  self.smtpSession.authType;

  [self.smtpSession setConnectionLogger:^(void * connectionID, MCOConnectionLogType type, NSData * data){
    NSLog(@"MCOSMTPSession: [%i] %@", type, [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]);
  }];

  [[self.imapSession checkAccountOperation] start:^(NSError *error) {
    if (nil == error) {
      success();
    } else {
      failure(error); //FAILS WITH INVALID CREDENTIALS ERROR
    }
  }];
}

Once again, the above code works fine, unless the application has not been used in some time. I was not sure if I needed to refresh the OAuthToken or not, so I tried doing the following on launch of the application:

GTMOAuth2Authentication *auth = [GTMOAuth2ViewControllerTouch authForGoogleFromKeychainForName:KeychainItemName clientID:GmailAppClientID clientSecret:GmailClientSecret];

  BOOL canAuthorize = [auth canAuthorize]; //returns YES
  NSDictionary *googleSettings = [[EmailServicesInfo emailServicesInfoDict] objectForKey:Gmail];

  [[ModelManager sharedDefaultInstance] authenticateWithEmailAddress:[auth userEmail] oAuthToken:[auth refreshToken] imapHostname:[googleSettings objectForKey:IMAPHostName] imapPort:[[googleSettings objectForKey:IMAPPort]integerValue] smtpHostname:[googleSettings objectForKey:SMTPHostName] smtpPort:[[googleSettings objectForKey:SMTPPort]integerValue] type:EmailProtocolTypeImapAndSmtpGMail success:^(Account *account) {

    //create IMAP session

} failure:^(NSError *error) {
  NSLog(@"failure %@", error);
}];

But I still get the same error. I have no idea why the OAuth token stops working or how to resolve this. Since the user is able to save multiple accounts, I am wondering if I need to save the refresh token for each account in core data and use that if the access token stops working?

Foi útil?

Solução

(Disclaimer - I don't know iOS or the gtm-oauth2 libraries, but I do know Google's OAuth implementation.)

Conceptually you do need to persist the refresh token for the user. The refresh token is a long-lived credential which is used (along with your client secret) to get a short-lived access token that is used for actual API calls.

If you anticipate making multiple calls in a short period of time then your app will normally actually persist both the refresh token and access token (currently access tokens will last 1 hour).

That all said, it looks like the gtm-oauth2 library should be taking care of persisting these already (looks like authForGoogleFromKeychainForName does this).

What I think you need help with is getting an up-to-date access token that you can use to initiate your IMAP session.

The gtm-oauth2 library does contain an authorizeRequest method. It takes information about an HTTP request you intend to make and adds the appropriate authorization headers. It looks like this code will examine the state of the access token, and refresh it if necessary.

While I know you aren't able to make an HTTP request (you need to speak IMAP), my suggestion is to use this method with a dummy NSMutableURLRequest - and then, once it's finished, don't actually send the HTTP request, instead examine the headers it added and pull the access token from there.

See: https://code.google.com/p/gtm-oauth2/wiki/Introduction#Using_the_Authentication_Tokens

Hope that helps - I don't have an iOS environment to test it on.

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top