Skip to content

iOS Appsync + Cognito User Pools + S3 = Authenticated File Download?

0

I have shifted my S3 bucket to be authenticated users only, but I cannot seem to get the user's authenticated status to the AWSS3 component. All the AppSync/DynamoDB setup works properly and S3 is working in an UnAuth public setup to download files. My users login, authenticate, then are provided only data they are permitted to via various Pipeline resolvers. I am seeking to replicate this for the file download portion. Users are given a folder (String) via permissions from a DynamoDB query. They are also assigned to a User Pool Group, which I could further use to ensure they are limited to their specific folder.

I have attempted to follow various guides that suggest a link between AppSync + DynamoDB, and S3 by using an S3ObjectInput MimeType in order to upload a file, which then seems to allow a user to download the file via an authenticated key (String) from DynamoDB. However, I can't find any code examples actually implementing this using the AWSS3 package. I also cannot figure out a way to inject the AppSync/Cognito token into any download method of the AWSS3 package.

I understand upgrading to Amplify is recommended, however I'm trying to get things working before rolling over. The AppSync to Amplify documentation seems rough/minimal at best. I'm not against implementing an Amplify client just to handle the S3 component, if there's a direct way to implement that.

I highly appreciate help. I've been attempting this for a number of days without any real progress and have scoured the internet for solutions.

Error received:

AWSCognitoIdentityErrorDomain Code=8 "(null)" UserInfo={__type=NotAuthorizedException, message=Unauthenticated access is not supported for this identity pool.}]

S3 Setup: My S3 Bucket has "Block all public access" ON Policy:

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "AWS": [
                    "arn:aws:iam::33333333:role/service-role/my_authRole"
                ]
            },
            "Action": [
                "s3:GetObject",
                "s3:GetObjectAttributes",
                "s3:GetObjectVersion"
            ],
            "Resource": [
                "arn:aws:s3:::mybucket",
                "arn:aws:s3:::mybucket/*"
            ]
        }
    ]
}

Cognito Setup: UserA is in MyUserPool, which belongs to MyIdentityPool. MyIdentityPool has my_authRole

iOS Appsync:

            // Initialize the AWS AppSync client
            let serviceConfig = try AWSAppSyncServiceConfig()
            let serviceConfigCognito = try AWSAppSyncServiceConfig(forKey: "Default")
            let cacheConfigCognito = try AWSAppSyncCacheConfiguration(
                useClientDatabasePrefix: true,
                appSyncServiceConfig: serviceConfigCognito
            )
            let clientConfigCognito = try AWSAppSyncClientConfiguration(
                appSyncServiceConfig: serviceConfigCognito,
                userPoolsAuthProvider: MyCognitoUserPoolsAuthProvider(),
                cacheConfiguration: cacheConfigCognito
            )

            appSyncClient = try AWSAppSyncClient(appSyncConfig: clientConfigCognito)
        } catch {
            print("Error initializing appsync client. \(error)")
        }
        if let userInformation = UserManager().getUserInformation() {
            self.userID = userInformation.userID
        }

class MyCognitoUserPoolsAuthProvider : AWSCognitoUserPoolsAuthProvider {
    func getLatestAuthToken() -> String {
     let pool = AWSCognitoIdentityUserPool(forKey: CognitoIdentityUserPoolId)
     let session =  pool?.currentUser()?.getSession()
        if let token = session?.result?.idToken {
            return token.tokenString
        } else {
            return ""
        }
    }
}

Attempt 1 (working with unauth):

    func downloadFile(folderID: String, fileName: String, onCompletion: ((DownloadError?) -> Void)?) {

        let expression = AWSS3TransferUtilityDownloadExpression()
        expression.progressBlock = {(task, progress) in
            })
        }
        
        let resourceDocPath = (FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)).last! as URL
        let pdfNameFromUrl = "\(fileName + ".pdf")"
        
        let actualPath = resourceDocPath.appendingPathComponent(pdfNameFromUrl)
        //Create a completion handler to be called when the transfer completes
        var completionHandler: AWSS3TransferUtilityDownloadCompletionHandlerBlock?
        completionHandler = { (task, location, data, error) -> Void in
            DispatchQueue.main.async(execute: {
                if let error = error {
                    onCompletion?(DownloadError.error)
                    print("Download failed with error: \(error)")
                } else{
                    do {
//                        print("File saved to: ", actualPath)
                        try data?.write(to: actualPath, options: .atomic)
                        onCompletion?(nil)
                    } catch {
                        onCompletion?(DownloadError.error)
                    }
                }
                })
        }
        
        let transferUtility = AWSS3TransferUtility.default()
               transferUtility.downloadData(
                   forKey: folderID + "/" + fileName + ".pdf",
                   expression: expression,
                   completionHandler: completionHandler).continueWith { (task) -> AnyObject? in
                if let error = task.error {
                    NSLog("Error: %@",error.localizedDescription);
                    DispatchQueue.main.async(execute: {
                        //self.statusLabel.text = "Failed"
                    })
                }
        }
    }

Attempt 2 (getting file info just for starters)

    func getS3FileDate(folderID: String, fileName: String, onCompletion: @escaping GetS3InfoResponse) -> Void {
        
        let credentialProvider = AWSCognitoCredentialsProvider(regionType: MyRegion, identityPoolId: MyIdentityPoolID)
        let configuration = AWSServiceConfiguration(region: MyRegion, credentialsProvider: credentialProvider)
        AWSServiceManager.default().defaultServiceConfiguration = configuration
        AWSS3.register(with: configuration!, forKey: "defaultKey")
        
        let s3 = AWSS3.s3(forKey: "defaultKey")
        let headObjectsRequest: AWSS3HeadObjectRequest = AWSS3HeadObjectRequest()
        headObjectsRequest.bucket = "MyBucket"
        headObjectsRequest.key = folderID + "/" + fileName + ".pdf"
        headObjectsRequest.ifModifiedSince = Date()
        
        s3.headObject(headObjectsRequest) { ( response: AWSS3HeadObjectOutput?, error: Error?) -> Void in
            if let responseDate = response?.lastModified {
                onCompletion(responseDate, nil)
            }
            if error != nil {
                onCompletion(nil, DownloadError.error)
            }
        }
    }
2 Answers
0

To enable authenticated file downloads from S3 using AppSync, Cognito User Pools, and the AWSS3 package in iOS, you need to properly configure the AWSS3 client with the authenticated credentials. Here's how you can modify your code to achieve this:

  1. First, ensure that your Cognito Identity Pool is properly configured to assume the correct IAM role (my_authRole) for authenticated users.

  2. Modify your S3 setup code to use the Cognito credentials:

func configureS3() {
    let credentialsProvider = AWSCognitoCredentialsProvider(regionType: .YourRegion, identityPoolId: "YourIdentityPoolId")
    let configuration = AWSServiceConfiguration(region: .YourRegion, credentialsProvider: credentialsProvider)
    AWSS3.register(with: configuration!, forKey: "defaultS3")
}
  1. Update your download function to use the authenticated S3 client:
func downloadFile(folderID: String, fileName: String, onCompletion: ((DownloadError?) -> Void)?) {
    let transferUtility = AWSS3TransferUtility.s3TransferUtility(forKey: "defaultS3")
    
    let expression = AWSS3TransferUtilityDownloadExpression()
    expression.progressBlock = { (task, progress) in
        // Handle progress
    }
    
    let resourceDocPath = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).last!
    let pdfNameFromUrl = "\(fileName).pdf"
    let actualPath = resourceDocPath.appendingPathComponent(pdfNameFromUrl)
    
    let completionHandler: AWSS3TransferUtilityDownloadCompletionHandlerBlock = { (task, location, data, error) in
        DispatchQueue.main.async {
            if let error = error {
                print("Download failed with error: \(error)")
                onCompletion?(.error)
            } else if let data = data {
                do {
                    try data.write(to: actualPath, options: .atomic)
                    onCompletion?(nil)
                } catch {
                    onCompletion?(.error)
                }
            }
        }
    }
    
    transferUtility.downloadData(
        forKey: "\(folderID)/\(fileName).pdf",
        expression: expression,
        completionHandler: completionHandler
    ).continueWith { (task) -> Any? in
        if let error = task.error {
            print("Error: \(error.localizedDescription)")
        }
        return nil
    }
}
  1. To use the Cognito user's group for further access control, you can modify your S3 bucket policy to include conditions based on the Cognito group. You would need to pass the group information in the credentials when assuming the IAM role.

  2. Call the configureS3() function in your app's initialization process, after setting up the Cognito user pool.

  3. When you need to download a file, simply call the downloadFile() function with the appropriate parameters.

This approach uses the Cognito Identity Pool to obtain temporary AWS credentials for the authenticated user, which are then used to access S3. The S3 bucket policy you've set up will ensure that only authenticated users with the correct IAM role can access the files.

Remember to handle token refresh and error cases appropriately in your app. Also, ensure that your Cognito setup (User Pool, Identity Pool, and IAM roles) is correctly configured to work with your S3 bucket policy.
Sources
Using Amazon Cognito as an identity provider with AWS Transfer Family and Amazon S3 | AWS Storage Blog
Developer Resources | Amazon Cognito
Write custom activity data with a Lambda function after Amazon Cognito user authentication using an AWS SDK - Amazon Cognito
Write custom activity data with a Lambda function after Amazon Cognito user authentication using an AWS SDK - AWS Lambda

answered 2 years ago

0

Up. Still looking for a solution.

answered 2 years ago

You are not logged in. Log in to post an answer.

A good answer clearly answers the question and provides constructive feedback and encourages professional growth in the question asker.