How do I use an iOS app's bundle identifier to 'authorize' upload to Google Cloud Storage? - ios

Our service is using Google App Engine as our backend, and we're now implementing an upload-function for images etc.
Using the answers from several different questions here on stack, I have made it working, but not completely as I want. We are not using the built-in OAuth etc, and for now we want the storage to be public, but not entirely public. We would like to limit it to users of our own app (I.E no authentication). In the Cloud-console we can create an API-key for iOS. When doing this, we copy the API-key to the app, and pass it along with every upload-request. This is currently working, when the bucket-permission is set to allUsers - WRITE
However, inside the API-key, we can supply our app's own Bundle Identifier, so that, supposedly, only requests from our app is allowed. (App Store ID/URL is also permitted, apparently).
Adding this bundle-id does nothing as long as the bucket has the permission allUsers - WRITE. If I change the bundle-id to not match the actual bundle-id, it still works. So which permission should it use for the bucket to make the bundle-id in the API-key apply? And what should be sent along in the upload-code on iOS (acl?)?.
If I remove the allUsers-permission, and use something else, I get this error when trying to upload:
{message:"There is a per-IP or per-Referer restriction configured
on your API key and the request does not match these
restrictions. Please use the Google Developers Console
to update your API key configuration if request from this
IP or referer should be allowed." data:[1] code:403}}
This is how I'm using it right now (though I have tried several different things, all picked up from different questions/answers):
GTLServiceStorage *serv = [[GTLServiceStorage alloc] init];
serv.additionalHTTPHeaders = [NSDictionary dictionaryWithObjectsAndKeys:
#"[my project id]", #"x-goog-project-id",
#"application/json-rpc", #"Content-Type",
#"application/json-rpc", #"Accept", nil];
serv.APIKey = #"[my iOS API key, gotten from console, (linked to bundle-id?)]";
serv.retryEnabled = YES;
GTLStorageBucket *bucket = [[GTLStorageBucket alloc] init];
bucket.name = #"[my bucket]";
GTLUploadParameters *params = [GTLUploadParameters uploadParametersWithFileHandle:fileHandle MIMEType:#"image/jpeg"];
GTLStorageObject *storageObject = [[GTLStorageObject alloc] init];
storageObject.name = #"testFile.jpg";
//I have no idea what I'm doing with the following stuff, but I've tried several things:
GTLStorageObjectAccessControl *objAccessControl
= [GTLStorageObjectAccessControl new];
//This is working
objAccessControl.entity = #"allUsers";
objAccessControl.email = #"[my app-id]#project.gserviceaccount.com";
objAccessControl.role = #"OWNER";
//If I try this instead, it is not working.
//objAccessControl.domain = #"[my app-id].apps.googleusercontent.com";
//objAccessControl.role = #"WRITER";
//Probably because it's bullshit, I have no idea what I'm doing.
storageObject.acl = #[objAccessControl];
[...] //Bucket and upload and stuff. It seems like it's the ACL-thing above that's not working..
It seems like I have to connect the permissions on the bucket to the iOS API Key somehow, but I don't know if it's even possible.
What I want: All users to be able to use the cloud, given that they are requesting it from my iOS app.

As this question never got an answer I'll add one here, based on the information currently in the post.
The reason you got the error 'There is a per-IP or per-Referer restriction..' when calling the GCS API with the iOS API Key is simply because the GCS API doesn't work with API Keys for private data, only Bearer Tokens (ie. using OAuth). There isn't anything you could have done to make the API Key work with the GCS API directly with private data. The reason it worked when you had 'allUsers - WRITE' set as the ACL is simply because that ACL allows public access.
To access the private data without user intervention requires a Service Account, however the Google APIs Objective-C Client only supports OAuth2 Client IDs. The rationale being that Service Accounts are intended for server-side authentication only. Using a Service Account in a client would involve distributing the private key along with the app, which could easily be compromised. For reference, here's a sample of how you might authorize the GCS service using OAuth:
NSString *keychainItemName = #"My App";
NSString *clientID = <your-client-id>;
NSString *clientSecret = <your-client-secret>;
// How to check for existing credentials in the keychain
GTMOAuth2Authentication *auth;
auth = [GTMOAuth2WindowController authForGoogleFromKeychainForName:kKeychainItemName
clientID:clientID
clientSecret:clientSecret];
...
// How to set up a window controller for sign-in
NSBundle *frameworkBundle = [NSBundle bundleForClass:[GTMOAuth2WindowController class]];
GTMOAuth2WindowController *windowController;
windowController = [GTMOAuth2WindowController controllerWithScope:kGTLAuthScopeStorageDevstorageFullControl
clientID:clientID
clientSecret:clientSecret
keychainItemName:kKeychainItemName
resourceBundle:frameworkBundle];
[windowController signInSheetModalForWindow:[self window]
completionHandler:^(GTMOAuth2Authentication *auth,
NSError *error) {
if (error == nil) {
self.storageService.authorizer = auth;
}
}];
...
// Initialize service with auth
GTLServiceStorage *serv = [[GTLServiceStorage alloc] init];
serv.authorizer = auth;
All of this was taken from the storage sample on the google-api-objectivec-client GitHub page, so you can refer to it for a complete example with context.
This still leaves the question of how to implement access to GCS from iOS without user authorization. The short answer to this is that the iOS Key can be used to restrict access to your own backend API hosted on Google Cloud Endpoints, and that backend application can authorize against GCS using a Service Account (usually the Application Default Service Account). The Cloud Storage Client Library Examples page has samples using the default credentials for different languages.
Further details on how to implement an Endpoints API for this purpose are probably getting outside of the scope of this question, but this should serve as a good starting point.

Related

Is it possible change username in Amazon Cognito with iOS SDK?

as written in the title is it possible change username of a Amazon Cognito user? I can't find anything in documentation
It is possible to update the preferred_username of a Cognito User using the iOS SDK, using the updateAttributes API call. However, kindly note that you would not be able to modify the username of a user. Quoting the official AWS documentation,
The username value is a separate attribute and not the same as the
name attribute. A username is always required to register a user, and
it cannot be changed after a user is created.
But, the preferred_username value can indeed be changed, and a sample code to change the preferred username using the iOS SDK, as per the official documentation is stated as follows:
AWSCognitoIdentityUserAttributeType * attribute = [AWSCognitoIdentityUserAttributeType new];
attribute.name = #"preferred_username";
attribute.value = #"John User";
[[user updateAttributes:#[attribute]] continueWithSuccessBlock:^id _Nullable(AWSTask<AWSCognitoIdentityUserUpdateAttributesResponse *> * _Nonnull task) {
//success
return nil;
}];
I would also like to state that the AWS API documentations for the iOS SDK are rather minimal, and I would recommend developers to go through the SDK source code whenever in doubt.

OVER_QUERY_LIMIT in simple SPGooglePlacesAutocompleteQuery request

I'm trying to create simple autocomplete UI widget:
self.autocompleteQuery = [[SPGooglePlacesAutocompleteQuery alloc] initWithApiKey:[GlobalConfig sharedInstance].kGoogleBrowserKey];
self.autocompleteQuery.language = kFFAutocompleteQueryLanguage;
self.autocompleteQuery.types = SPPlaceTypeAddress;
self.autocompleteQuery.location = [FFAppDataHelper coordinatesForMoscow];
self.autocompleteQuery.radius = [GlobalConfig sharedInstance].kMoscowRadius;
self.autocompleteQuery.countryCode = #"RU";
Then I want to get all values for input string from UITextField on 'valueChanged' event:
self.autocompleteQuery.input = [streetName stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
[self.autocompleteQuery fetchPlaces:^(NSArray *places, NSError *error) {
[self loaderStopAnimating];
if (places) {
//do some stuff
} else {
FFError *detectedError = [FFError errorWithNSError:error];
[self showErrorMessage:[detectedError errorMessage]];
}
}];
So when I type 'k' I get response with streets and if I add next char to my text field I receive OVER_QUERY_LIMIT every time. I've tried it on simulator and devices with the same result. And it starts working again after 10-20 sec. I don't use loops or smth similar, I just want to get suggestions for input string in real time, but I can't get it because of error. What should I do to avoid it?
For the web service request, use a key.
Follow this :https://developers.google.com/maps/documentation/geocoding/start#api_key
Doc says:
Note: Maps for Work users must include client and signature parameters with their requests instead of a key.
All Geocoding API applications should use an API key. Including a key in your request:
Allows you to monitor your application's API usage in the Google Developers Console. Enables per-key instead of per-IP-address quota limits. Ensures that Google can contact you about your application if necessary. The Geocoding API uses an API key to identify your application. API keys are managed through the Google APIs console. To create your key:
Visit the APIs console at Google Developers Console and log in with your Google Account. Click the Services link from the left-hand menu in the APIs Console, then activate the Geocoding API service. Once the service has been activated, your API key is available from the API > Access page, in the Simple API Access section. Geocoding API applications use the Key for server apps. To specify a key in your request, include it as the value of a key parameter.
Note: By default, a key can be used from any server. We strongly recommend that you restrict the use of your key by IP address to servers that you administer. You can specify which IP addresses are allowed to use your API key by clicking the Edit allowed referers... link in the API console.
Note: HTTPS is enforced for requests that include an API key.

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.

AWSS3PresignedURLErrorDomain, iOS, AWSSDK v2

Using AWSSDK V2 with iOS 8, went through sample code bases and setup
cognito with proper access to my S3Bucket. following example here
https://github.com/awslabs/aws-sdk-ios-samples/tree/master/S3BackgroundTransfer-Sample/Objective-C
When I try the upload I get this error
Error: Error Domain=com.amazonaws.AWSS3PresignedURLErrorDomain Code=1
"accessKey in credentialProvider can not be nil" UserInfo=0x7d905610
{NSLocalizedDescription=accessKey in credentialProvider can not be nil}
I made sure the role used by cognito poold id has access to my s3 bucket based on the policy I created for it. What could be the problem?
Agfter sebastians commnent, I went back and verified that I am using unauthenticated users, with a role that has access to my s3bucket, and then looked at the last comment he made about cognito being async and about its initialization. I am doing this in this method here
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
AWSCognitoCredentialsProvider *credentialsProvider = [AWSCognitoCredentialsProvider credentialsWithRegionType:CognitoRegionType
identityPoolId:CognitoIdentityPoolId];
AWSServiceConfiguration *configuration = [AWSServiceConfiguration configurationWithRegion:DefaultServiceRegionType
credentialsProvider:credentialsProvider];
[AWSServiceManager defaultServiceManager].defaultServiceConfiguration = configuration;
// Override point for customization after application launch.
return YES;
}
now when breaking on the line
[AWSServiceManager defaultServiceManager].defaultServiceConfiguration = configuration;
and inspecting in debugger the properties of credentialsProvider, It does have a lot of nil properties !!
credentialsProvider AWSCognitoCredentialsProvider * 0x7886a0d0 0x7886a0d0
NSObject NSObject
_useEnhancedFlow BOOL YES '\x01'
_identityId NSString * nil 0x00000000
_accessKey NSString * nil 0x00000000
_secretKey NSString * nil 0x00000000
_sessionKey NSString * nil 0x00000000
_expiration NSDate * nil 0x00000000
I did create the entity pool and embed the ID in my code, what should I look for here?
I had the exact same problem, however the answers above didn't help me either.
Changing
let CognitoRegionType = AWSRegionType.Unknown
let DefaultServiceRegionType = AWSRegionType.Unknown
To
let CognitoRegionType = AWSRegionType.EUWest1
let DefaultServiceRegionType = AWSRegionType.EUWest1
In Constants.swift solved it for me. Of course this is in the context of the sample application mentioned above.
From the error above, it looks like your CognitoCredentialsProvider has no access key / secret key.
When using Cognito with authenticated users, just creating an IdentityPool and creating an AWSCognitoCredentialsProvider instance is not enough.
You must write code to authenticate your user. Currently Cognito supports user authentication with Amazon, Facebook, Google, any generic OpenID Connect or your own backend.
Once you receive an authentication token from the Identity Provider, you can give it to Cognito (using its .logins property), Cognito will then exchange that token for a valid Access Key and Secret Key.
It looks like your Cognito Identity provider has no access key / secret key yet. The example you linked above is an example of Cognito usage for unauthenticated users - which might be valid for your application, or not. Depending on your use case.
Also remember that CognitoIdentitProvider class is asynchronous (like all AWS iOS SDK classes). Ensure that you are not calling your S3 Transfer Manager class before Cognito is fully initialized.
To better understand Cognito Identity, I would invite you to read http://docs.aws.amazon.com/mobile/sdkforios/developerguide/cognito-auth.html
after much inspection, I found the problem ( lack of documentation ).
I needed to create an online policy for the role used by cognito. when going to IAM services -> roles -> click on role -> permissions, under that tab there are two types of policies managed and inline, before I setup a managed policy which didn't do anything, so I went back and created an inline policy and that is where I give this role access to the S3 bucket, and now it works !!
Hope this helps someone else, and AWS really need to document better.

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

Resources