Apigee user authentication, access_token, and cache login session in iOS - ios

I'm new to Apigee and can't seem to wrap my head around it. I am familiar with implementing an iOS app that talks to a database via a webservice call. The call would involve passing back and forth JSON or variables though POST, GET, etc.
The user flow I envision is a lot like Facebook long term token storage. Here are the steps:
Type username and password to login.
The app will remember the access_token in the keychain.
The access_token will be used with any future requests such as updating profile. This way the user doesn't have re-login every time he/she is using the app.
Log out will clear all the token.
If the token is invalid or expired, the app will take the user back to login.
I've taken multiple routes and ended up getting stuck on all of them when it comes to Apigee.
ROUTE 1
I made a call to logInUser and receive access_token in return.
[self.apigeeDataClient logInUser:username password:password];
All this is good until I want to update user's email address using the code below.
NSMutableDictionary *requestDict = [NSMutableDictionary dictionary];
[requestDict setObject:email forKey:kDataEmail];
NSString *url = [NSString stringWithFormat:#"%#/%#/%#/users/%#?access_token=%#", BASE_URL, UG_ORG_NAME, APP_NAME, [userData objectForKey:kDataUUID], self.accessToken];
NSString *op = #"PUT";
NSError *error;
NSData *jsonData = [NSJSONSerialization dataWithJSONObject:[NSDictionary dictionaryWithDictionary:requestDict]
options:0
error:&error];
[self.apigeeDataClient apiRequest:url operation:op data:[NSString stringWithUTF8String:[jsonData bytes]]];
It seems that every other time it's giving me "No content to map to Object due to end of input" error. I checked out this thread but have no luck. I made sure the JSON object is not null. I tried changing the operation from PUT to POST and GET. None of which update the email address in the database.
ROUTE 2
Instead of using apiRequest, I switched to updateEntity.
NSMutableDictionary *requestDict = [NSMutableDictionary dictionary];
[requestDict setObject:email forKey:kDataEmail];
[requestDict setObject:kDataUsers forKey:kDataType];
[requestDict setObject:self.accessToken forKey:kDataAccessToken];
NSString *entityID = [userData objectForKey:kDataUUID];
[self.apigeeDataClient updateEntity:entityID entity:requestDict];
It looks promising except I started getting "Subject does not have permission" like the issue described in this thread. I tried calling assignPermissions like mentioned in Apigee document but that didn't solve the problem. I even provide access_token with the call, even though I shouldn't have to.
In the attempt to avoid calling login also tried calling storeOAuth2TokensInKeychain and retrieveStoredOAuth2TokensFromKeychain mentioned here. That didn't work either.
The only thing way to resolve this error is by calling logInUser before making a call to updateEntity. This means the user will have to login every time he/she wants to use the app. I know I can store username/password in the keychain. But before I do that I'm wondering if there's better solution out there.
I know it's a long post. So thank you for reading this far. Any pointers are greatly appreciated.

Related

Evernote iOS SDK - How do I authenticate with a token?

I am using Evernote SDK for iOS and I am saving the authentication token when the user has authorized access.
Once the user installs my application on a different device, I want to use that token to reauthenticate automatically, but it looks like SDK doesn't support that. Is there a way to do that?
I had the same issue last week, and their SDK indeed doesn't support it out-of-the-box, but after some research I found a solution that works perfectly. This solution mimics a valid authentication flow.
A little background:
When the ENSession class initializes, it retrieves the credentials that are saved on the keychain (unless [[ENSession sharedSession] unauthenticate] was called earlier). The problem is that the keychain is empty when using a different device, so our goal is to add a valid ENCredentials instance to the ENCredentialStore.
Solution:
Add the following imports to your code: ENCredentials.h and ENCredentialStore.h. We will need them later.
Initialize the ENSession like you already do, using setSharedSessionConsumerKey:(NSString *)key consumerSecret:(NSString *)secret optionalHost:(NSString *)host.
In order to create a valid ENCredentials object, we need to provide the following objects:
NSString * host
NSString * edamUserId
NSString * noteStoreUrl
NSString * webApiUrlPrefix
NSString * authenticationToken
NSDate * expirationDate
The host is always www.evernote.com (as defined in ENSession under ENSessionBootstrapServerBaseURLStringUS).
edamUserId is the user id you received when you got the original token. Same for the expirationDate. If you are not sure how to get them then you should use [[ENSession sharedSession].userStore getUserWithSuccess:^(EDAMUser *user) once authenticated.
So the only objects that are actually missing are noteStoreUrl and webApiUrlPrefix. Their format is always:
noteStoreUrl: https://www.evernote.com/shard/edam_shard/notestore
webApiUrlPrefix: https://www.evernote.com/shard/edam_shard/
Luckily, your token already contains edam_shared (value of S=, see this):
#"S=s161:U=5ce3f20:E=1561182201b:C=24eb9d000f8:P=285:A=app:V=2:H=e8ebf56eac26aaacdef2f3caed0bc309"
If you extract s161 and put it in the URLs above it will work (I am sure you know how to extract that, but let me know if you're having problems).
Now we are ready to authenticate using the token. First, expose the necessary functions from ENSession using a category:
#interface ENSession(Authentication)
- (void)startup;
- (void)addCredentials:(ENCredentials *)credentials;
#end
And authenticate using the token:
ENCredentials *credentials = [[ENCredentials alloc] initWithHost:ENSessionBootstrapServerBaseURLStringUS edamUserId:userId noteStoreUrl:noteStoreUrl webApiUrlPrefix:webApiUrlPrefix authenticationToken:token expirationDate:expirationDate];
[[ENSession sharedSession] addCredentials:credentials];
[[ENSession sharedSession] startup];
The last line is important in order to refresh the ENSession and retrieve the new stored credentials.
Now you are authenticated and ready to query the SDK. Good luck.

Use post request to receive LinkedIn user profile information in iOS

I am looking for a way to retrieve the profile information of a LinkedIn user by making a post request, I have read these two LinkedIn pages but it doesn't seem to explain much, or I couldn't understand much of it:
REST API LinkedIn
basic profile informations
I have seen these example on stackoverflow but I didn't understood to much:
http://api.linkedin.com/v1/people/~:(id,first-name,last-name,maiden-name,email-address,formatted-name,phonetic-last-name,location:(country:(code)),industry,distance,current-status,current-share,network,skills,phone-numbers,date-of-birth,main-address,positions:(title),educations:(school-name,field-of-study,start-date,end-date,degree,activities))
I am only interested to retrieve the skills section the one that on the website appears as so:
There's a fantastic library for LinkedIn-iOS integration by Kirsten Jones, you can use that to make calls to the LinkedIn API. You need an access token to make calls.
https://github.com/PrincessPolymath/LinkedIn-OAuth-Sample-Client
Make calls like this:
NSURL *url = [NSURL URLWithString:#"http://api.linkedin.com/v1/people/~:(id,first-name,last-name,maiden-name,formatted-name,phonetic-last-name,location:(country:(code)),industry,distance,current-status,current-share,network,skills,phone-numbers,date-of-birth,main-address,positions:(title),educations:(school-name,field-of-study,start-date,end-date,degree,activities))"]];
OAMutableURLRequest *request =
[[OAMutableURLRequest alloc] initWithURL:url
consumer:oAuthLoginView.consumer
token:oAuthLoginView.accessToken
callback:nil
signatureProvider:nil];
[request setValue:#"json" forHTTPHeaderField:#"x-li-format"];
OADataFetcher *fetcher = [[OADataFetcher alloc] init];
[fetcher fetchDataWithRequest:request
delegate:self
didFinishSelector:#selector(profileApiCallResult:didFinish:)
didFailSelector:#selector(profileApiCallResult:didFail:)];
- (void)profileApiCallResult:(OAServiceTicket *)ticket didFinish:(NSData *)data
{
NSString *responseBody = [[NSString alloc] initWithData:data
encoding:NSUTF8StringEncoding];
NSDictionary *profile = [responseBody objectFromJSONString];
if ( profile )
{
name.text = [[NSString alloc] initWithFormat:#"%# %#",
[profile objectForKey:#"firstName"], [profile objectForKey:#"lastName"]];
headline.text = [profile objectForKey:#"headline"];
.....and get skills and other user details
}
}
Use tool:https://apigee.com/console/linkedin
API to get the list of Skills:
https://api.linkedin.com/v1/people/~:(id,num-connections,skills)?format=json
I tried it in the Console tool and able to fetch the skills for my profile. I think the individual skill ID with above request should help you to get further information. Try it out.
If anyone is reading this answer, the LinkedIn API has limited access as of May 2015. You will need to apply to their Apply with LinkedIn program to access full profile fields including skills.
https://developer.linkedin.com/docs/fields
I have fair amount of experience working with the linkedIn API. It's been a little while, but hopefully this will set you on the right track.
To get profile information from a linked in user who is one of your connections you need a format like follows:
NSString *theRequest = [NSString stringWithFormat:#"https://api.linkedin.com/v1/people/id=abc123:(first-name,last-name,picture-url,location:(name))?oauth2_access_token=%#&format=json", accessToken];
This request will return the first name, last name, profile picture url, and location of the user with the id specified.
You can see a list of field types you can request by checking out linkedIn's documentation on their profile field descriptions here:
https://developer.linkedin.com/docs/fields
If you're wondering how to get the id of the user whose profile information you want request in the first place, you can make request for some basic info (including ids) of all your connections like this:
NSString *basicConnectionInfo = [NSString stringWithFormat:#"https://api.linkedin.com/v1/people/~/connections:(id,first-name,last-name)?oauth2_access_token=%#&format=json", accessToken];
This request will give you the id, first name, and last name of all of your connections. After you've gotten the id of the person you want, you can make a request using the user's id (as shown in the first example).
Now for the slightly unfortunately news... If you followed the link provided above, you'll notice that the skills field is part of the "Member profile fields available to Apply with LinkedIn developers". I'm assuming you'll have to follow this link they provided: https://developer.linkedin.com/docs/apply-with-linkedin in order to access the skills member profile field.
I have not applied with LinkedIn. So, I haven't tested a call to the skills field. But, I'm guessing it'll be similar to the examples I've shown you. Hope this helps!

How do I upload files to Google Cloud Storage in Objective-C/iOS?

I've been looking for documentation on how to upload files to my 'bucket' in Google Cloud Storage from my iOS app, but I can't find anything at all on the subject. No documentations, no tutorials, no example projects. Am I completely blind? I am simply trying to find a way for my app-users to upload a file to a "public bucket", and get a URL in return. All I can find is chunks of HTTP-protocols or JSON etc, and I have no idea how to use that, but there's no reference to it either. It feels like the author of those documentations expects me to know everything already. I've found some OSX-example codes, but they are too without documentation, and I've been trying to read the code they have provided, but with no luck.
What I'm looking for is something like this:
(This code is made up. It's what I want. I noticed Google used the prefix GTL* for their classes)
NSData *dataToUpload = ... ; //Or UIImage or some movie-format or whatever
NSURL *destination;
GTLStorageUploader *uploader = [GTLStorageUploader alloc]initWithBucket:#"myBucket" withHashOrKeyOrSomething:#"a1b2c3hashkeyOrWhatever"];
destination = [uploader uploadData:dataToUpload];//inBackground etc..
It's actually easier than this when using Parse.com, but there's simply not enough storage space for my app there, so I need to be able to upload the data files to Google Cloud Storage. How?
I did eventually get this to work. It wasn't pretty, though.
It's quite a long time ago now, so I can't really remember all the logic etc, but I'll post what made it work, and change the ID-stuff. I hope it helps, and sorry I didn't remember to write this when I found out and it was fresh in mind. I don't have time to get into this at the moment.
An important note: I also had to change a lot of permissions on the bucket and on the users/authorized on GoogleCloudStorage to make this work. We tried so many different combinations, but I THINK this was the stuff we did:
On each bucket: "Allow everyone to upload/delete/edit etc".
On the auth for the entire CloudStorage: "Allow only entities with certain accessToken to access this CloudStorage.
Allow only www.yourAppEngineURL.com to request such an accessToken.
This felt wrong, and still does. If anyone gets a hold of this accessToken, they can do whatever they want as long as that accessToken is valid. Like.. delete all files. But that was the only way we could make it work. Of course, only authorized users could request this accessToken from OUR appEngine, but still.. meh. I'm no security-guru, and this was just a fun project, so we let it go. Now to the code.
When uploading:
GTLServiceStorage *serv = [[GTLServiceStorage alloc] init];
serv.additionalHTTPHeaders = [NSDictionary dictionaryWithObjectsAndKeys:
#"123123123", #"x-goog-project-id",
#"application/json-rpc", #"Content-Type",
#"application/json-rpc", #"Accept", nil];
GTLStorageObjectAccessControl *objAccessControl = [GTLStorageObjectAccessControl new];
objAccessControl.entity = #"project-owners-123456"; //Some value from the control panel on CloudStorage or something. Or Apps.. Or AppEngine, not sure.
//Probably connected to the accessToken my AppEngine requests and sends to users.
objAccessControl.role = #"OWNER";
GTLStorageObjectAccessControl *objAccessControl2 = [GTLStorageObjectAccessControl new];
objAccessControl2.entity = #"allUsers";
objAccessControl2.role = #"READER";
//Don't remember why I need both. Or what they do. Hey ho.
//It looks like.. Everybody can read. Only my authorized accessToken-people can write? probably.
GTLStorageBucket *bucket = [[GTLStorageBucket alloc] init];
bucket.name = #"my_bucket";
NSError *err;
NSFileHandle *fileHandle = [NSFileHandle myLocalFileURLiThink error:&err];
if(err)
{
NSLog(#"Some error here");
}
GTLUploadParameters *params = [GTLUploadParameters uploadParametersWithFileHandle:fileHandle MIMEType:#"video/mp4 (or something else)"];
GTLStorageObject *storageObject = [[GTLStorageObject alloc] init];
storageObject.acl = #[objAccessControl, objAccessControl2];
NSString *key = [NSString stringWithFormat:#"fileName.mp4"];
// should probably be unique.. The url will be cloud.com/my_bucket/fileName.mp4
//You can generate something unique by putting together the user's userID and the timeStamp for the dateTime right now.
//This user will never upload two things within this second.
storageObject.name = key;
After this point in the code I do some magic I'm not gonna post. I ask for and receive an accessToken to use on GoogleCloudStorage from our own API.
I don't remember where or how I got that token to begin with, but I believe that the backend (AppEngine) had to request it from the CloudStorage-thing, using a pretty standard call.
And, as I said in the beginning, we changed some settings on CloudStorage making our AppEngine the only entity allowed to request this token. Or something.. This token has a lifecycle of like.. 15 minutes or so.. I don't know, it's provided by some Google-default-thing. I might look into it later if any of you need it.
NSString *receivedAccessToken = #"abc123"; //Received from CloudStorage via AppEngine.
NSString *accessToken = #"Bearer %#", receivedAccessToken" //(pseudo) Because it needed to say "Bearer " first. Don't know why, or how I found out..
[serv setAdditionalHTTPHeaders:[NSDictionary dictionaryWithObject:accessToken forKey:#"Authorization"]];
//Upload-magic:
GTLQueryStorage *query = [GTLQueryStorage queryForObjectsInsertWithObject:storageObject bucket:bucket.name uploadParameters:params];
GTLServiceTicket *t = [serv executeQuery:query completionHandler:^(GTLServiceTicket *ticket, id object, NSError *error) {
//Handle error first.
NSLog(#"Success!");
dispatch_async(dispatch_get_main_queue(), ^{
actualURL = [NSString stringWithFormat:#"%#%#", yourFullBucketURL /*( e.g www.googleCloud.com/my_bucket)*/, key /*file.mp4*/];
});
}];
And you can track the progress of the upload after this block, with the ticket-object (like, t.uploadProgressBlock = ...).
This code has been edited quite a bit for Stack-purposes now, so I might have screwed up something, so I probably doesn't work exactly like this. But read it, and try to understand how it works. Good luck. If you have the option, stay with Amazon or something else, this was not fun to work with. Worst documentation ever. Also, Amazon's S3 uploads/downloads faster than GoogleCloudStorage. I regret changing from Amazon to Google. Amazon had so much better API too, almost like the one in my question.
Here's the code used by AppEngine to request the AccessToken:
private GoogleCredential getGoogleCredential(String scope) throws GeneralSecurityException, IOException
{
JsonFactory JSON_FACTORY = JacksonFactory.getDefaultInstance();
HttpTransport httpTransport = GoogleNetHttpTransport.newTrustedTransport();
GoogleCredential credential = new GoogleCredential.Builder()
.setTransport(httpTransport)
.setJsonFactory(JSON_FACTORY)
.setServiceAccountId(Constants.SERVICE_ACCOUNT_EMAIL)
.setServiceAccountPrivateKeyFromP12File(new File(Constants.CLOUD_STORAGE_KEY))
.setServiceAccountScopes(Collections.singleton(scope))
.build();
return credential;
}
The parameter "scope" sent in to this method is either https://www.googleapis.com/auth/devstorage.read_only or https://www.googleapis.com/auth/devstorage.read_write
The method returns a GoogleCredential-object, on which you can call googleCredential.refreshToken(). I believe this is the call made to get the token. I'm not sure though.
The Constants (email and key) are stuff you need to set up and get from the auth-page on Google Cloud Storage, I think.
This documentation should cover some of it (it looks more documented now than it did then, I think): https://cloud.google.com/storage/docs/authentication

Empty permissions array not getting the right auth callback under iOS

Our iOS app required the 'publish_stream' permission. We're now removing it and passing in an empty string NSArray but it doesn't reflect in the auth screen.
It keeps telling the user "This app may post on your behalf, including status updates, photos and more." and we can't seem to reset that.
- (void)facebookLogin
{
NSArray *permissions = [NSArray arrayWithObjects: #"",nil]; // was #"publish_stream"
// Look here for a list of available permissions:
// http://developers.facebook.com/docs/reference/api/permissions/
[_facebook authorize:permissions];
}
We tried passing in nil instead of the empty string NSArray but that didn't help.
Are there auth options to be adjusted in the App Settings of the developers page?
Any ideas?
Thanks!
Gon
This is apparently a recent bug. See the developer forums here. Hopefully they'll be resolving it soon.

iPhone FBConnect, dashboard.addnews

How to add news via FBConnect?
I have the following code:
NSString *newsBody = #"[{\"message\": \"News message\" }]";
NSMutableDictionary *params = [NSMutableDictionary dictionaryWithObject:newsBody forKey:#"news"];
[[FBRequest requestWithDelegate:self] call:#"facebook.dashboard.addnews" params:params dataParam:nil];
After I sent the request I received the success responce. But I can't see the new news in the facebook account.
Also, I tried to add full info into news parameter (http://wiki.developers.facebook.com/index.php/Dashboard.addNews):
NSString *newsBody = #"[{\"message\": \"News message\",\"action_link\": {\"text\": \"link text\",
\"href\": \"http: //google.com\"} }]";
But this request returns error.
Any ideas?
I found the answer. The FBStreamDialog should be used to publish the news. It means that the custom interface cannot be used for this purposes. :(
Also, just wanted to say that FBConnect it's a one big mess!! Currently fighting with the bug when after clicking Cancel button in the FB dialog the dialog firing the event dialogDidSucceed but not dialogDidCancel. Such a big mess!!!

Resources