Onedrive SDK - Authentication Token is getting nil value - ios

During OneDrive Authentication using OneDrive SDK for iOS, after the authentication screen, At times, the highlighted values of “response” objects are NIL . I’m not sure why this is happening.
Out of 10 trials, 2-3 times the values inside the Repsonse object is NIL. So this is landing the App in crash.Any guess why this is happening? May be because of poor network? Or any other thing I might have missed? In Onedrive SDK - LiveAuthRequest.m file has the following lines of code.
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
id response = [LiveAuthHelper readAuthResponse:self.tokenResponseData];
if ([response isKindOfClass:[LiveConnectSession class]])
{
_client.session = response; => Crash is Happening Here*
self.session = response;
[self updateStatus:AuthTokenRetrieved];
}
else
{
self.error = response;
[self updateStatus:AuthFailed];
}
self.tokenResponseData = nil;
self.tokenConnection = nil;
}
The Response from the Onedrive Server side. 1. authentication_Token, 2.refreshToken, 3.scope 4.expires_in 5.tokenType",
iOS version:5.1.1
OneDrive SDK Version:5.0

Related

Google Drive on iOS and "invalid_grant"

I have an iOS app that uses Google Drive to sync some files. The app was working fine up until a couple of months ago (it uses iCloud by default for syncing so it wasn't until recently that I learned the Google Drive since was no longer working).
The error I'm getting is:
An error occurred: Error Domain=com.google.HTTPStatus Code=400 "(null)" UserInfo={json={
error = "invalid_grant";
}, data=<7b0a2020 22657272 6f722220 3a202269 6e76616c 69645f67 72616e74 220a7d>}
The OAuth part sets to be OK and I think I'm authorized to access the user's Google Drive. The error happens when I try to list the files at the top level to see if I need to create a special directory for my app.
- (void)createGoogleDocumentsDirIfNeeded:(id<GoogleServiceDelegate>)delegate
{
NSString *parentID = #"root";
GTLQueryDrive *query = [GTLQueryDrive queryForFilesList];
query.q = [NSString stringWithFormat:#"'%#' in parents and title = '%#' and trashed != true", parentID, APP_NAME];
[googleService executeQuery:query completionHandler:^(GTLServiceTicket *ticket,
GTLDriveFileList *files,
NSError *error)
{
if (error == nil)
{
if ( files.items.count == 0)
{
NSLog(#"Creating google documents dir: %#", APP_NAME);
[self createGoogleDocumentsDir:delegate];
}
else
{
GTLDriveFile *file = [files objectAtIndexedSubscript:0];
self.googleDocumentsID = file.identifier;
NSLog(#"Directory exists: %#; fileID = %#", APP_NAME, googleDocumentsID);
[delegate googleDocumentsDirCreated:YES];
}
}
else
{
NSLog(#"An error occurred: %#", error);
}
}];
}
The error is displayed in the last NSLog().
I'm pretty convinced that something has change on Google's side that is causing this. I've reverted to code to a point where I know it worked before and it is still failing.
Possibly I've made some change in our Google Developer's Console that is causing this, but I can't see what.
It is also the case that the Google API Dashboard still shows some successful accesses, so it seems this is not failing for everyone.
Any thoughts as to what could be going on? The "invalid_grant" error seems pretty generic and web searches show it happens in lots of situations. Any help would be appreciated.
As you say, there could be a variety of explanations.
Some to check are:-
has the user withdrawn his permission
are you using an expired refresh token
is the phone's clock accurate
Read https://developers.google.com/identity/protocols/OAuth2UserAgent
The URL used when authenticating a user is https://accounts.google.com/o/oauth2/v2/auth. This endpoint is accessible over SSL, and HTTP connections are refused.
Only for httpS connects for now

Switch from unauth to developer authenticated cognito user - AWS iOS SDK

Overall Problem:
I have a problem using a developer authenticated identity with my front end (iOS). I know my backend produces the correct token and identityID but my refresh method never gets called. I've also looked at the sample but I get slightly confused with everything going on.
Flow Explanation:
Currently I have a login screen that has a login button. The user presses the login button, then my api class takes the credentials, encrypts the password and stores it in keychain (commented out for now since it doesn't work on simulator). My DeveloperAuthenticatedIdentityProvider is called my app BusytimeAuthenticated. I have completed all the methods (I'm using AWS lambda and DynamoDB to authenticate users so) I start with unauthenticated access which allows me to access only two methods, login and signup. Then I want to assume my authenticated user which allows me to call my other methods.
my API Code:
[AWSLogger defaultLogger].logLevel = AWSLogLevelVerbose;
id<AWSCognitoIdentityProvider> identityProvider = [[BusytimeAuthenticated alloc] initWithRegionType:AWSRegionUSEast1
identityId:nil
identityPoolId:#"SOMEIDENTITYPOOLID"
logins:#{#"SOMEPROVIDERNAME": #"SOMEUSERNAME"}
providerName:#"SOMEPROVIDERNAME"
];
credentialsProvider = [[AWSCognitoCredentialsProvider alloc] initWithRegionType:AWSRegionUSEast1
identityProvider:identityProvider
unauthRoleArn:nil
authRoleArn:nil];
configuration = [[AWSServiceConfiguration alloc] initWithRegion:AWSRegionUSEast1
credentialsProvider:self.credentialsProvider];
AWSServiceManager.defaultServiceManager.defaultServiceConfiguration = configuration;
[[credentialsProvider refresh] continueWithBlock:^id(BFTask *task){
[self testAuth];
return nil;
}];
my DeveloperAuthenticatedIdentityProvider code (BusytimeAuthenticated) :
#import "BusytimeAuthenticated.h"
#interface BusytimeAuthenticated()
#property (strong, atomic) NSString *providerName;
#property (strong, atomic) NSString *token;
#end
#implementation BusytimeAuthenticated
#synthesize providerName=_providerName;
#synthesize token=_token;
- (instancetype)initWithRegionType:(AWSRegionType)regionType
identityId:(NSString *)identityId
identityPoolId:(NSString *)identityPoolId
logins:(NSDictionary *)logins
providerName:(NSString *)providerName{
if (self = [super initWithRegionType:regionType identityId:identityId accountId:nil identityPoolId:identityPoolId logins:logins]) {
self.providerName = providerName;
}
return self;
}
// Return the developer provider name which you choose while setting up the
// identity pool in the Amazon Cognito Console
- (BOOL)authenticatedWithProvider {
return [self.logins objectForKey:self.providerName] != nil;
}
// If the app has a valid identityId return it, otherwise get a valid
// identityId from your backend.
- (BFTask *)getIdentityId {
// already cached the identity id, return it
if (self.identityId) {
return [BFTask taskWithResult:nil];
}
// not authenticated with our developer provider
else if (![self authenticatedWithProvider]) {
return [super getIdentityId];
}
// authenticated with our developer provider, use refresh logic to get id/token pair
else {
return [[BFTask taskWithResult:nil] continueWithBlock:^id(BFTask *task) {
if (!self.identityId) {
return [self refresh];
}
return [BFTask taskWithResult:self.identityId];
}];
}
}
// Use the refresh method to communicate with your backend to get an
// identityId and token.
- (BFTask *)refresh {
if (![self authenticatedWithProvider]) {
return [super getIdentityId];
}else{
// KeychainWrapper *keychain = [[KeychainWrapper alloc]init];
AWSLambdaInvoker *lambdaInvoker = [AWSLambdaInvoker defaultLambdaInvoker];
NSDictionary *parameters = #{#"username" : #"SOMEUSERNAME",
#"password":#"SOMEENCRYPTEDPASS",
#"isError" : #NO};
NSLog(#"Here");
[[lambdaInvoker invokeFunction:#"login" JSONObject:parameters] continueWithBlock:^id(BFTask* task) {
if (task.error) {
NSLog(#"Error: %#", task.error);
}
if (task.exception) {
NSLog(#"Exception: %#", task.exception);
}
if (task.result) {
self.identityId = [task.result objectForKey:#"IdentityId" ];
self.token = [task.result objectForKey:#"Token" ];
// [keychain mySetObject:[task.result objectForKey:#"Token" ] forKey:#"Token"];
// [keychain mySetObject:[task.result objectForKey:#"IdentityId" ] forKey:#"IdentityId"];
NSLog(#"Result: %#", task.result);
}
return [BFTask taskWithResult:self.identityId];
}];
}
return NULL;
}
#end
Summary Problem:
Unfortunately when I test my new priveleges, I see from the error: "Unauth_Role/CognitoIdentityCredentials is not authorized to perform: lambda:InvokeFunction". Clearly I'm not switching properly. I've placed a breakpoint in my refresh method to see if it's getting called. It's not. I'm not quite understanding how I switch properly. Any help with getting this to work is much appreciated.
Note: One big change I did make though is I took out the "DeveloperAuthenticationClient" class because I assumed I could do it without it.
The fundamental problem is that you are trying to call a Lambda function (which requires credentials) to get credentials. Because you are using the "default" client configuration, when your developer authenticated client comes back with a response it is going to override the credentials used to access your Lambda function. Additionally, once that id has been transitioned to authenticated, you won't be able to use it to get credentials in an unauth flow and would need to generate a new unauthenticated id just to authenticate again and then get back to your authenticated id.
I would strongly encourage you to setup API Gateway in front of your Lambda function to remove this circular dependency.
Update based on new information in the question...
A few things here:
1. Avoid code like while(!finished) to wait on an async task to complete. In the best case, this style of busy waiting will consume a CPU/core at 100% while doing nothing useful and adversely affect battery life and will only hurt performance of your app. Instead, use a notification with a block. Since you have already have a AWSTask in this instance, instead of returning nil at the end of the [credentialsProvider refresh] continueWithBlock... just call your [self testAuth] right there and do away with the finished/while code.
2. In your getIdentityId implementation the first if condition checks if there is an identityId and if there is it returns nil. I'm guessing you goal here is to cache the identityId after a successful authentication and return that so that you don't have to call your backend every time getIdentityId is called. If that is the case, pretty sure you want to return identityId instead of nil
3. I don't think this is the cause of your issue but will simplify things: As long as you've configured your identity pool with Auth/UnAuth roles in the console, you don't have to explicitly use them when initializing the AWSCognitoCredentialsProvider.
Once these are resolved if you continue to have problems, please debug the code in more detail and tell us things like the following:
Does the refresh method get called? If so, which parts of your if statement does it enter and what is the result? Does it ever enter the else block and call your backend identity provider? Does it successfully retrieve an identity id and return it?
If you get further but start experiencing a slightly different issue then please mark this question answered and post a separate question instead of continuing to edit this question. This will help keep things clear (this question/answer is getting pretty long and has changed).
Original answer to initial posted question/code... The getIdentity method of the AWSCognitoCredentialsProvider returns an AWSTask (i.e. a BFTask). So you'll need to call something like continueWithBlock in order to actually execute the method. In the first block of code above it looks like you're not doing that.

Quickblox user search from multiple sources

I have a requirement to search QuickBlox users whose IDs match their
Facebook IDs OR
Twitter IDs OR
Login IDs
So I am firing multiple QBUser queries at once, using following code:
[QBUsers usersWithLogins:[NSArray arrayWithObject:m_searchString] delegate:self];
[QBUsers usersWithFacebookIDs:[NSArray arrayWithObject:m_searchString] delegate:self];
[QBUsers usersWithTwitterIDs:[NSArray arrayWithObject:m_searchString] delegate:self];
So for example, if I give search string as "Testuser" - it should search all users having login = Testuser, FB login = Testuser, and Twitter login = Testuser.
Based on source of request (FB/Twitter/My own app), I need to put results in different UI parts.
The problem is, I can't differentiate which result comes back for which request.
-(void)completedWithResult:(Result*)result
{
[self showActivityIndicator:NO];
// QuickBlox User creation result
if([result isKindOfClass:[QBUUserPagedResult class]])
{
// Success result
if(result.success)
{
}
}
}
I can see that above code is hit 3 times. But I don't see anything in QBUUserPagedResult class that tells me from which request this result has come.
Something like a tag for request should suffice, but I am not sure what thing it is, looking at the documentation.
Is there anything I can use?
Alternately, is there another approach to what I am trying to achieve (instead of multiple requests)?
I figured it out that context option which is an NSString works like a tag for each Quickblox request:
[QBUsers usersWithLogins:[NSArray arrayWithObject:m_searchString] delegate:self context:MY_STRING_CONSTANT];
Then in delegate function:
- (void)completedWithResult:(Result *)result context:(void *)contextInfo
{
if(result.success && [result isKindOfClass:QBUUserPagedResult.class])
{
NSString * _context = (__bridge NSString *) contextInfo;
if([_context isEqualToString:MY_STRING_CONSTANT])
{
}
}
}
As simple as that, but the documentation doesn't say much about it, or it's not visible as it should be. I had to dig into their forums to figure it out.

iOS Parse Stripe Integration

I'm fairly new to programming and I created an app to charge customers and would like to store their CC information and charge it at a later time. I've been going through all the tutorials and documentation and I am unable to follow how I can integrate this into my app. Do I need to know other technical skills such as Rest API, Curl, Ruby, etc to get this set up? All the guides and documentation is pointing to that direction. I don't really understand what GET/POST is for and how that fits into iOS Objective-C programming.
Any guidance on how to set this up would be tremendously appreciated. I've been stuck on this for some time now.
Parse's stripe API is not as complete as it could and should be. There are many features it does not include natively, but can be accomplished VIA an HTTP Request. I had to learn a little bit of Javascript, and HTTP request to get many features working. Of course your first instinct should tell you do NOT store a CC number on any device ever! Anytime you have a user input a CC number, immediately get a token and then that is all you will need to use.
Luckily stripe gives you the ability to save customers, and attached CC to customers, and then charge that customer in the future without getting the CC number again. Parse's api does not handle adding a CC to a customer so I added the feature myself.
So Step 1 and 2 Generate a Customer using Parse's API, and generate a Token from the CC information they enter again using Parse's API. If you need help with this, and the cloud code required let me know.
Step 3 Add a CC to a customer. I'm using a custom Customer object, but the main thing you really need is the stripe customerId which is customer.identifier in my code, and tokenID from your CC which in my case is token.tokenId. The response back will be a JSON string with the card information, I turn this into a Dictionary, and then create a STPCard from the dictionary. Also I show how to remove a card from a customer.
iOS Code:
+(void)addToken:(STPToken *)token toCustomerId:(NSString *)customerId completionHandler:(PFIdResultBlock)block
{
[PFCloud callFunctionInBackground:#"stripeUpdateCustomer" withParameters:#{#"customerId":customerId,#"data":#{#"card":token.tokenId}} block:block];
}
+ (void)removeCard:(STPCard *)card FromCustomer:(ELCustomer *)customer completion:(STPCardDeletionBlock)handler
{
if (!customer ||!customer.identifier || !card || !card.identifier || !handler) [NSException raise:#"RequiredParameter" format:#"Required Parameter Missing for deleting card from customer"];
[PFCloud callFunctionInBackground:#"stripeDeleteCardFromCustomer" withParameters:#{#"cardId":card.identifier,#"customerId":customer.identifier} block:^(id object, NSError *error)
{
NSDictionary *dict = nil;
NSError *jsonError = nil;
if (object && [object isKindOfClass:[NSString class]] && !error) {
dict = [NSJSONSerialization JSONObjectWithData:[object dataUsingEncoding:NSUTF8StringEncoding] options:kNilOptions error:&jsonError];
}
if (!jsonError && dict) {
handler(dict[#"id"],[dict[#"deleted"] boolValue],error);
}
else if(jsonError) handler(nil,NO,jsonError);
else handler(nil,NO,error);
}];
}
Cloud Code Required:
Parse.Cloud.define("stripeUpdateCustomer", function(request, response)
{
Stripe.Customers.update
(
request.params["customerId"],
request.params["data"],
{
success:function(results)
{
console.log(results["id"]);
response.success(results);
},
error:function(error)
{
response.error("Error:" +error);
}
}
);
});
Parse.Cloud.define("stripeDeleteCardFromCustomer", function(request, response)
{
Stripe.initialize(STRIPE_SECRET_KEY);
Parse.Cloud.httpRequest({
method:"DELETE",
//STRIPE_SECRET_KEY will be your stripe secrect key obviously, this is different from the public key that you will use in your iOS/Android side.
// STRIPE_API_BASE_URL = 'api.stripe.com/v1'
url: "https://" + STRIPE_SECRET_KEY + ':#' + STRIPE_API_BASE_URL + "/customers/" + request.params.customerId + "/cards/" + request.params.cardId,
success: function(httpResponse) {
response.success(httpResponse.text);
},
error: function(httpResponse) {
response.error('Request failed with response code ' + httpResponse.status);
}
});
});
iOS Code for applying a charge to a customer or token notice the required parameters in the dictionary are an amount in cents not dollars, a currency, and then either a customer or a tokenId. Note a customer can have many credit cards, but one of them is the active credit card. The active card is the card that will be charged when you charge a customer:
//Will attempt to charge customer, if no customer exists, or it fails to charge the custoemr it will attempt to charge a card token directly;
//*********Warning: This is the final step it will APPLY A CHARGE TO THE ACCOUNT.***************
-(void)processChargeThroughStripeWithCompletionHandler:(STPChargeCompletionHandler)handler
{
if (![self validForCardProcessing] && ![self validForCustomerProcessing]) {
handler(nil,[NSError errorWithDomain:MY_ERROR_DOMAIN code:elErrorCodeNoCustomerOrTokenID userInfo:[NSDictionary dictionary]]);
return;
}
[self processChargeThroughStripeUsingCustomerWithCompletionHandler:^(STPCharge *charge, NSError *error)
{
if (!error) handler(charge,error);
else{
[self processChargeThroughStripeUsingCardWithCompletionHandler:^(STPCharge *charge, NSError *error) {
handler(charge, error);
}];
}
}];
}
//Process payment using a customer to their active card. No token is required if customer exists with a card on record.
//*********Warning: This is the final step it will APPLY A CHARGE TO THE ACCOUNT.***************
-(void)processChargeThroughStripeUsingCustomerWithCompletionHandler:(STPChargeCompletionHandler)handler
{
if (!self.validForCustomerProcessing)
{
handler(self,[NSError errorWithDomain:MY_ERROR_DOMAIN code:elErrorCodeNoCustomerID userInfo:[NSDictionary dictionary]]);
return;
}
[PFCloud callFunctionInBackground:#"chargeToken" withParameters:[STPCharge dictionaryFromSTPChargeForProccessingUsingCustomer:self] block:^(id object, NSError *error)
{
if (!error)
{
[self initSelfWithDictionary:object];
NSLog(#"object:%#",object);
}
handler(self,error);
}];
}
//Process payment using a token that is attached to the charge, when complete self will be updated with the new charge information
//*********Warning: This is the final step it will APPLY A CHARGE TO THE ACCOUNT.***************
-(void)processChargeThroughStripeUsingCardWithCompletionHandler:(STPChargeCompletionHandler)handler
{
if (!self.validForCardProcessing)
{
handler(self,[NSError errorWithDomain:MY_ERROR_DOMAIN code:elErrorCodeNoTokenID userInfo:[NSDictionary dictionary]]);
return;
}
[PFCloud callFunctionInBackground:#"chargeToken" withParameters:[STPCharge dictionaryFromSTPChargeForProccessingUsingCard:self] block:^(id object, NSError *error)
{
if (!error)
{
[self initSelfWithDictionary:object];
}
handler(self,error);
}];
}
+ (NSDictionary *)dictionaryFromSTPChargeForProccessingUsingCard:(STPCharge *)charge
{
NSMutableDictionary *dictionary = [NSMutableDictionary dictionary];
dictionary[#"amount"] = charge.amountInCents;
dictionary[#"currency"] = charge.currency;
dictionary[#"card"] = charge.token.tokenId;
return dictionary;
}
+ (NSDictionary *)dictionaryFromSTPChargeForProccessingUsingCustomer:(STPCharge *)charge
{
NSMutableDictionary *dictionary = [NSMutableDictionary dictionary];
dictionary[#"amount"] = charge.amountInCents;
dictionary[#"currency"] = charge.currency;
dictionary[#"customer"] = charge.customer.identifier;
return dictionary;
}
Cloud code for charging a customer/token:
Parse.Cloud.define("chargeToken",function(request,response)
{
Stripe.initialize(STRIPE_SECRET_KEY);
Stripe.Charges.create
(
request.params,
{
success:function(results)
{
response.success(results);
},
error:function(error)
{
response.error("Error:" +error);
}
}
);
});
How are you storing their CC information to charge it at a later time? Before proceeding, you need to know if it is PCI compliant or not. At most, the only things you should be looking to store is the expiration date, last 4 digits, and an associated record object that Parse Stripe gives you that corresponds to that CC. Do not try to store the full CC.
As to your other questions:
Generally you need to know a web language to do something like this. Here is an example of a possible stack that I've seen in a situation like this:
iOS App -> sends request to Server (rails, python, php, etc) -> Will send request to 3rd party site
3rd party site response -> Server -> iOS app.
The point of the server is to intercept the call from the mobile App to Parse, and the response from Parse back to the mobile app. The reason for this is so you can have a "master" db of the transactions/states and can recover if the app is ever reinstalled on the user's phone. It also will let you store an identifier that points to the user's CC on parse stripe (I'm assuming).
You should really understand GET/POST as they are becoming a very basic feature of any iOS app. They are simply how you get/insert records from a server. Considering almost all of the popular apps have some kind of network connectivity embedded in them, it really is a core part of iOS programming IMO.

Google OAuth Login Error: Invalid credentials

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?
(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.

Resources