AWSS3TransferUtility uploading with Cognito authentication configuration issues? - swift

I have an app that authenticates with Cognito and has been working fine.
Now I need to reuse the authentication and returned token to upload files to S3. My understanding is that AWSS3TransferUtility is the way to go at this point. Only it is not clear what needs to be done and how is the token to be passed to S3? Can anyone point to an example? Just using examples available suggesting to do:
let credentialsProvider =
CredentialsProvider(regionType:region, identityPoolId:poolId)
let serviceS3Configuration = AWSServiceConfiguration(region:region, credentialsProvider: credentialsProvider)
AWSServiceManager.default().defaultServiceConfiguration = serviceS3Configuration
results in "Unauthenticated access is not supported for this identity pool" assuming that this is because token is not being used and S3 AWS service is not initialized correctly. But I do not see a way to set it ? what am I missing? I can see some examples suggesting setting logins property of credentialsProvider to
AWSCognitoLoginProviderKey but seems to be outdated at this point.
Any and all help would be greatly appreciated.

S3 configuration with Cognito is a little mysterious. The answers are in the docs but not entirely obvious. The core of getting this to work is registering your AWSMobileClient instance with the configuration.
Simplified code without error checking:
Boot up your AWSMobileClient:
AWSMobileClient.sharedInstance().initialize({ { userstate, error in
if userstate != nil {
registerAuthentication(credentialsProvider: AWSMobileClient.sharedInstance())
}
})
Once completed pass the sharedInstance to AWSServiceConfiguration because AWSMobileClient is-a AWSCredentialsProvider
let DefaultTransferUtilityKey = "DEFAULT_AUTH_KEY"
func registerAuthentication(credentialsProvider: AWSCredentialsProvider) {
/// only do this once per app launch
/// assumes you're using the plist config method
guard let s3tranferInfo = AWSInfo.default().defaultServiceInfo("S3TransferUtility"),
let bucketName = s3tranferInfo.infoDictionary["Bucket"] as? String else {
assertionFailure("failed to load /S3TransferUtility/Bucket key - is awsconfiguration.json correct ?")
return
}
let transferConfig = AWSS3TransferUtilityConfiguration()
transferConfig.bucket = bucketName
if let serviceconfiguration = AWSServiceConfiguration(region: s3tranferInfo.region, credentialsProvider: credentialsProvider) {
AWSS3TransferUtility.register(with: serviceconfiguration, transferUtilityConfiguration: transferConfig, forKey: DefaultTransferUtilityKey)
}
}
and once that registration is actually finished you can access the transfer utility via the common key string.
lazy var transferUtility: AWSS3TransferUtility = {
let utility = AWSS3TransferUtility.s3TransferUtility(forKey: DefaultTransferUtilityKey)
return utility
}()
Bucket name and region could be strings also but if you're using AWSMobileClient you probably have the plist setup.

Related

AWSCognito Facebook Login Swift

I'm trying to use AWSCognito to sign in with a Facebook Access Token.
My identity pool is set up & I have the Facebook Access Token.
Unfortunately, the documentation is very limited for this. Has anyone successfully got a Facebook user logged in with AWSCognito?
So far I have;
let provider: Dictionary = [AWSIdentityProviderFacebook : accessToken.authenticationToken]
let credentialProvider = AWSCognitoCredentialsProvider(regionType: .EUWest2, identityPoolId: POOL_IDENTITY_STRING)
I'm assuming I need to pass-in the provider somewhere.
I tried to do this;
credentialProvider.identityProvider.logins() = provider
but I'm getting: "Cannot assign to value: function call returns immutable value"
Yes, I did something just like this recently.
You will want to init your credential provider using an identity provider manager.
credentialsProvider = AWSCognitoCredentialsProvider(
regionType: xxx,
identityPoolId: yyy,
identityProviderManager: idpm)
However, you can't just pass in the dictionary ["graph.facebook.com" : <access token>] as the third parameter.
You need something that conforms to the AWSIdentityProviderManager protocol, which basically just returns a task returning a dictionary.
I ended up making a simple wrapper class for this, see below.
import AWSCognito
class myIdentityProvider: NSObject, AWSIdentityProviderManager {
var keytokens: [String:String]?
init (logins: [String:String]) {
keytokens = logins
}
func logins () -> AWSTask<NSDictionary> {
let task = AWSTask(result: keytokens as AnyObject?)
return task as! AWSTask<NSDictionary>
}
}
So, then your code would be rewritten:
let provider: Dictionary = [AWSIdentityProviderFacebook : accessToken.authenticationToken]
let idpm = myIdentityProvider(provider)
let credentialProvider = AWSCognitoCredentialsProvider(
regionType: .EUWest2,
identityPoolId: POOL_IDENTITY_STRING,
identityProviderManager: idpm)

cannot authenticate user for aws appsync with swift SDK

I am trying to connect to my AWS AppSync service from my Swift mobile app using the AWS Swift SDK but keep getting the following error:
Error occurred: (401 unauthorized) Did not receive a successful HTTP code.
I am using User Pools and have set everything up following the tutorial for swift. My question is, how do I incorporate the AppSync.json config file generated in the console in my request? That is not mentioned in the tutorial and may be the reason I cannot connect.
The json file looks like this:
{
"graphqlEndpoint": "my_endpoint_url",
"region": "us-east-1",
"authenticationType": "AMAZON_COGNITO_USER_POOLS",
"apiKey": null
}
At the moment I am using the following configuration:
// Set up Amazon Cognito credentials
let credentialsProvider = AWSCognitoCredentialsProvider(regionType: CognitoIdentityRegion,
identityPoolId: CognitoIdentityPoolId, identityProviderManager: pool)
// You can choose your database location, accessible by SDK
let databaseURL = URL(fileURLWithPath:NSTemporaryDirectory()).appendingPathComponent(database_name)
do {
// Initialize the AWS AppSync configuration
let appSyncConfig = try AWSAppSyncClientConfiguration(url: AppSyncEndpointURL,
serviceRegion: AppSyncRegion,
credentialsProvider: credentialsProvider,
databaseURL:databaseURL)
appSyncClient = try AWSAppSyncClient(appSyncConfig: appSyncConfig)
// Set id as the cache key for objects
appSyncClient?.apolloClient?.cacheKeyForObject = { $0["id"] }
} catch {
print("Error initializing appsync client. \(error)")
}
EDIT #1
It turns out the example is using the API key method rather than user pools. So now I have changed the config to:
let appSyncConfig = try AWSAppSyncClientConfiguration(url: AppSyncEndpointURL, serviceRegion: AppSyncRegion, userPoolsAuthProvider: CognitoUserPoolsAuthProvider(pool: pool))
The problem is the message now is:
Use of unresolved identifier 'CognitoUserPoolsAuthProvider'
if I try:
let appSyncConfig = try AWSAppSyncClientConfiguration(url: AppSyncEndpointURL, serviceRegion: AppSyncRegion, userPoolsAuthProvider: AWSCognitoUserPoolsAuthProvider(pool: pool))
the error is:
'AWSCognitoUserPoolsAuthProvider' cannot be constructed because it has no accessible initializers
Not sure how to satisfy the userPoolsAuthProvider: argument in the config.
To address your specific issue, userPoolsAuthProvider needs to accept a class that extends the AWSCognitoUserPoolsAuthProvider protocol. So your instantiation would look something like this:
let appSyncConfig = try AWSAppSyncClientConfiguration(url: AppSyncEndpointURL,
serviceRegion: AppSyncRegion,
userPoolsAuthProvider: self,
databaseURL:databaseURL)
And then in the class in which you are creating the AppSyncClient, you would extend like this:
extension YourClass: AWSCognitoUserPoolsAuthProvider {
func getLatestAuthToken() -> String {
var token: String = ""
// pool is an instance of AWSCognitoIdentityUserPool
self.pool?.currentUser()?.getSession().continueOnSuccessWith(block: { (task) -> Any? in
token = task.result!.idToken!.tokenString
return nil
}).waitUnitFinished()
}
return token
}
Also I think the AppSync configuration by default looks for an awsconfiguration.json file in your project. You posted this a while back so possibly things have changed with AWS services and AppSync.
Hope this helps

SyncCredentials Don't exist in Realm Swift

Im trying to connect my app to a Realm Object Server. The documentation says, to do this you use the below:
// create a configuration object
let realmUrl = URL(string: "realms://example.com:9000/~/settings")!
let realmUser = SyncCredentials.usernamePassword(username: username, password: password)
let config = Realm.Configuration(user: realmUser, realmURL: realmUrl)
// open the Realm with the configuration object
let settingsRealm = try! Realm(configuration: config)
However for SyncCredentials.usernamePassword, XCode says SyncCredentials doesn't exist. From the looks you need to set SyncConfiguration on in Realm.configuration (or the only file I can find RealmConfiguration.swift)
Now I'm in that file theres no option to use SyncCredentials
My question is, how do I simply connect my app with a Realm Object Database using SyncCredentials (or however you're supposed to do it). Been pulling my hair out all day over this, surely it can't be that hard :-(
There seem some mistakes.
SyncCredentials is auth info that is used to log in. It is not user object. Realm.Configuration doesn't receive user and realmURL parameters in the initializer. You need to use SyncConfiguration instead.
The example code for logging in or instantiating Realm with existing user is the following.
let syncServerURL = URL(string: "realm://example.com:9080/~/settings")!
let syncAuthURL = URL(string: "http://example.com:9080")!
if let user = SyncUser.current {
let config = Realm.Configuration(syncConfiguration: SyncConfiguration(user: user, realmURL: syncServerURL))
let realm = try! Realm(configuration: config)
...
} else {
let username = ...
let password = ...
let credentials = SyncCredentials.usernamePassword(username: username, password: password)
SyncUser.logIn(with: credentials, server: syncAuthURL) { user, error in
DispatchQueue.main.async {
if let user = user {
let config = Realm.Configuration(syncConfiguration: SyncConfiguration(user: user, realmURL: syncServerURL))
let realm = try! Realm(configuration: config)
...
} else {
// Handle error
}
}
}
}
Please read our documentation again https://realm.io/docs/swift/latest/#sync
and also our RealmTasks sample project helps you to understand interacting Realm Object Server. https://github.com/realm-demos/realm-tasks
If you still see SyncCredentials not found error in above code, probably you didn't setup RealmSwift framework correctly. Please add more info that how did you set up Realm.
Swift Package Manager doesn't load SyncCredentials or SyncUser properly.
I was using the SPM to install RealmSwift as a dependency from IceCream. I removed Realm and IceCream from SPM. Then installed using Carthage to solve the issue.

Validate AWS Cognito Token with Swift

I have nearly completed the process for a developer authenticated sign in using AWS. I cannot seem to authenticate the back-end token that I receive and cannot seem to find any current implementations that are performing developer authentication via a third-party back-end. The error that I get is listed below.
As of right now my code looks like this:
Class containing Custom identity provider:
import Foundation
import AWSCognitoIdentityProvider
class CustomIdentityProvider: NSObject, AWSIdentityProviderManager {
var tokens: [NSString: NSString]?
init(tokens: [NSString: NSString]) {
self.tokens = tokens
}
#objc func logins() -> AWSTask<NSDictionary> {
return AWSTask(result: tokens! as NSDictionary)
}
}
AWS-APIManager.swift {snippet}
/* obtained cognito token from my back-end via getOpenIdTokenForDeveloperIdentity*/
/* From here I my app receives an IdentityId and Token */
let client_cognito_id = String(describing: valid_response)
let session_token = json.dictionaryValue["Token"]!
let login_with_amazon = NSString(string: "cognito-identity.amazonaws.com")
let token = NSString(string: String(describing: session_token))
let customProviderManager = CustomIdentityProvider(tokens: [login_with_amazon: token])
let credentialsProvider = AWSCognitoCredentialsProvider(
regionType: self.AWS_REGION,
identityPoolId: "us-east-1:XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX",
identityProviderManager: customProviderManager
)
credentialsProvider.setIdentityProviderManagerOnce(customProviderManager)
credentialsProvider.getIdentityId().continue ({ (task: AWSTask!) -> AnyObject! in
if (task.error != nil) {
print("Error!!!: " + (task.error?.localizedDescription)!)
} else {
// the task result will contain the identity id
let cognitoId = task.result
print(cognitoId)
print("SUCCESS!!!")
}
return nil
})
}
For some odd reason odd reason I can cannot authenticate the token that I have received. I get an error "Invalid login token. Can't pass in a Cognito token.". I've tried to follow the documentation and piece together working code that I have found literally hundreds of sources and cannot seem to be able to move past this part of the authentication process. Any help would be greatly appreciated. Thanks!
I believe the issue here is that although you are supplying the token, you are not setting the identity id that you are getting from your backend. As such, it is calling GetId with a Cognito OpenIdConnectToken, which is not supported.
The simplest client implementation of Developer Authenticated Identities is to extend AWSCognitoCredentialsProviderHelper
Apologies for providing this in Objective C instead of Swift. In your implementation just override the token method.
- (AWSTask<NSString *> *)token {
//get the identity id and token from your server
//You can use AWSTaskCompletionSource if you don't have it and need to get it asynchronously.
//Once you have this information, simply set the identity id and return the token
self.identityId = identityId;
return [AWSTask taskWithResult:token];
}

How to invoke an AWS Lambda function in Swift

I can't find any documentation or examples on how to invoke a Lambda function in Swift but I've tried to extrapolate from the documentation using Objective-C and I'm still getting errors:
"Error in myFunction: ValidationException: Supplied AttributeValue is empty, must contain exactly one of the supported datatypes"
It appears that I'm not passing in the parameters to the function correctly when I invoke the lambda function from swift because the script tries to write to DynamoDB but one of the parameters is empty (this lambda script works when I invoke it in javascript/node).
let lambda = AWSLambda.defaultLambda()
let request = AWSLambdaInvocationRequest()
var context = [String: String]()
let jsonString = "{\"email\":\"example#example.com\",\"name\":\"example\"}"
let jsonData = jsonString.dataUsingEncoding(NSUTF8StringEncoding)
request.clientContext = jsonData?.base64EncodedStringWithOptions(NSDataBase64EncodingOptions.Encoding64CharacterLineLength)
request.functionName = "myFunction"
lambda.invoke(request).continueWithBlock( {
(currentTask: AWSTask!) -> AWSTask in
if (currentTask.error != nil) {
// failed to execute.
print("Error executing: ", currentTask.error)
task.setError(currentTask.error)
} else {
print("token: ", currentTask.result)
task.setResult(currentTask.result)
}
return currentTask
})
You need to set the payload parameter to a map containing the data you want to pass.
let invocationRequest = AWSLambdaInvokerInvocationRequest()
invocationRequest.functionName = "myFunction"
invocationRequest.invocationType = AWSLambdaInvocationType.RequestResponse
invocationRequest.payload = ["email" : "example#example.com", "name" : "example"]
let lambdaInvoker = AWSLambdaInvoker.defaultLambdaInvoker()
let task = lambdaInvoker.invoke(invocationRequest).continueWithSuccessBlock() { (task) -> AWSTask! in
print("response: ", task.result)
}
Ryan Fitzgerald's answer gives me multiple compile-time errors, but I've had success with this version:
First, I have an initialization function with access credentials. Note that this is not the recommended secure access method for production code, but it is fine for testing and other purposes. It also assumes you have a Constants.swift file where you define the listed constants:
func initializeLambda() {
let credentialsProvider = AWSStaticCredentialsProvider.init(accessKey:Constants.AWS_ACCESS_KEY, secretKey: Constants.AWS_SECRET_KEY)
let defaultServiceConfiguration = AWSServiceConfiguration(region: Constants.AWS_REGION, credentialsProvider: credentialsProvider)
AWSServiceManager.defaultServiceManager().defaultServiceConfiguration = defaultServiceConfiguration
}
For the remainder we can provide a version similar to the previous version. I removed the 'let task' because 'task' is not used in his example. Additionally, I've included the logical outline of some JSON parsing that you are likely to be doing inside the invoke task. Finally, I've changed to a continueWithBlock(). If you use a continueWithSuccessBlock() you will not enter this block when Amazon Lambda reaches its timeout window or if something else goes wrong with the request and typically you do want these situations to be handled here.
self.initializeLambda() //Call our previously written initialization function
let invocationRequest = AWSLambdaInvokerInvocationRequest()
invocationRequest.functionName = "functionName"
invocationRequest.invocationType = AWSLambdaInvocationType.RequestResponse
invocationRequest.payload = ["key1" : "value1", "key2" : "value2"]
let lambdaInvoker = AWSLambdaInvoker.defaultLambdaInvoker()
lambdaInvoker.invoke(invocationRequest).continueWithBlock() { (task: AWSTask) -> AWSTask in
print("response: ", task.result)
//In here you'll likely be parsing a JSON payload
if let payload: AnyObject = task.result?.payload {
if let error: AnyObject = payload.objectForKey("error") {
//If there is an error key in the JSON dictionary...
} else {
//If the JSON dictionary has no error key...
}
return task;
}
}
Tested and verified as functional on Swift 2.2 in Xcode 7.3.
The answers from both the Ryan's were great and useful and I just want to add a couple of additional thoughts.
In most cases, before you can invoke a Lambda, you might need to authenticate, so the errors you get might not necessarily be because of your Lambda calls but due to failing authentication. With AWS, however, there are several different ways to authenticate and this will change based on the credentials you have.
Ryan Davis shows you one way where your backend team has set up an AWS Access Key and an AWS Secret Key.
In my case, I had to authenticate using AWS Cognito Identity Pools and there are also User Pool authentication so you need to figure out what credentials your team has given you and read the appropriate authentication documentation.
Since I needed to use using AWS Cognito Identity Pools, all I had was the region and the identity pool id so in Swift 5 authentication for AWS Cognito Identity Pools
let credentialsProvider = AWSCognitoCredentialsProvider(regionType: Constants.AWS_REGION,
identityPoolId: Constants.AWS_REGION.AWS_IDENTITY_POOL_ID)
let serviceConfiguration = AWSServiceConfiguration(region: Constants.AWS_REGION,
credentialsProvider: credentialsProvider)
AWSServiceManager.default().defaultServiceConfiguration = serviceConfiguration
And then the Lambda invocation more or less stays the same but just with slightly updated Swift 5 syntax:
if let invocationRequest = AWSLambdaInvokerInvocationRequest() {
invocationRequest.functionName = "function_name"
invocationRequest.invocationType = AWSLambdaInvocationType.requestResponse
invocationRequest.payload = ["key_1": "value_1"]
let lambdaInvoker = AWSLambdaInvoker.default()
lambdaInvoker.invoke(invocationRequest) { (awsLambdaInvokerInvocationResponse, error) in
guard let payload = awsLambdaInvokerInvocationResponse?.payload as? [String: String] else {
// Handle error here
return
}
if let userId = payload["message"] {
print("USR Id: \(userId)")
}
}
}
You will need to adjust your handling based on the structure of your payload returned to you by the Lambda, in my case it was:
{
"message": "user-id-8868-8475-8757"
}
Finally, remember to import the required libraries for your use case, for my above case I needed:
import AWSCore
import AWSLambda