Miro
Miro

Reputation: 8650

Cognito: upload file to S3 into a custom user subfolder from the browser

This is part question, part answering some issues I struggled with. Question is at the bottom.

I wanted to upload a file straight from the browser to S3 using Cognito. It worked great with unauthenticated users but then every user has access to the same bucket or at least same folder. I wanted each user to have access to their folder only.

After 3 days of head banging I was able to get Cognito to work with custom Developer Authenticated Identities using JavaScript and PHP in one compact php file.

On the server I use getOpenIdTokenForDeveloperIdentity to receive an IdentityId and a Token for a username johnsmith.

Then I immediately present the id and token to the client to authenticate with those and upload a file to S3:

<!-- BEGIN SERVER SIDE -->
<?php
session_start();

//Include AWS client libs
require ('vendor/autoload.php');
use Aws\CognitoIdentity\CognitoIdentityClient;
use Aws\Sts\StsClient;

/* Global Vars */
$aws_region = 'us-east-1';
$aws_key = 'JF4L3ELC4CAVQV4VAKIA';
$aws_secret = 'OKfoWZ91qZHBhIBzDZLINzHVs9Ymaxi689Ym3vT8';
$identity_pool_id = 'us-east-1:83024a7c-438e-aa29-8bac-ff6717d73ec5';

//Initialize a Cognito Identity Client using the Factory
$client = CognitoIdentityClient::factory(array('region' => $aws_region, 'key' => $aws_key, 'secret' => $aws_secret));

/* Acquire new Identity */
$identity = $client->getOpenIdTokenForDeveloperIdentity(array('IdentityPoolId' => $identity_pool_id, 'Logins' => array('my-custom-login' => 'johnsmith')));

//Obtain Identity from response data structure
$id = $identity->get('IdentityId');
$token = $identity->get('Token');

print_r($identity);
// Results in:
// [IdentityId] => us-east-1:c4db3eca-6ed0-af08-4aaa-a33aee2e994a
// [Token] => eyJraWQiOiJ1cy1lYXN0LTExIiwidHlwIjoiSldTIiwiYWxnIjoiUlM1MTIifQ.eyJzd...
?>

<!-- BEGIN CLIENT SIDE -->

<script src="https://sdk.amazonaws.com/js/aws-sdk-2.436.0.min.js"></script>

<script>
var bucketName = 'mybucket';

//Get CognitoIdentityCredentials
AWS.config.region = 'us-east-1';
AWS.config.credentials = new AWS.CognitoIdentityCredentials({
    IdentityId: '<?php echo $id; ?>',
    IdentityPoolId: '<?php echo $identity_pool_id; ?>',
        Logins: {
            //'my-custom-login':'johnsmith', //This does not work. It throws Error 400: Please provide a valid public provider.
            'cognito-identity.amazonaws.com':'<?php echo $token; ?>'
        }
});

//S3 Upload
var s3 = new AWS.S3({
    //region: 'us-east-1', //Bucket region - not required if same as AWS.config.region
    apiVersion: '2006-03-01',
    params: {Bucket: bucketName}
});


//Refresh and Upload
AWS.config.credentials.refresh(function(){

    var IdentityId = s3.config.credentials.params.IdentityId;
    var keyName = "cognito/"+IdentityId+"/it_works.txt";
    var params = {Bucket: bucketName, Key: keyName, Body: 'Hello World!'};
    s3.putObject(params, function (err, data) {
    if (err)
            console.log(err)
        else
            console.log("Successfully uploaded data to " + bucketName + "/" + keyName);
    });
});

//Console: Successfully uploaded data to mybucket/cognito/us-east-1:c4db3eca-6ed0-af08-4aaa-a33aee2e994a/it_works.txt);
</script>

My IAM permissions policy was borrowed from here and looks like this:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Action": ["s3:ListBucket"],
      "Effect": "Allow",
      "Resource": ["arn:aws:s3:::mybucket"],
      "Condition": {"StringLike": {"s3:prefix": ["${cognito-identity.amazonaws.com:sub}/*"]}}
    },
    {
      "Action": [
        "s3:GetObject",
        "s3:PutObject"
      ],
      "Effect": "Allow",
      "Resource": ["arn:aws:s3:::mybucket/${cognito-identity.amazonaws.com:sub}/*"]
    }
  ]
}

This all works great and uploads a file in mybucket/cognito/us-east-1:c4db3eca-6ed0-af08-4aaa-a33aee2e994a/it_works.txt

What I wanted was to set a custom sub-folder named after the username johnsmith.

My question is:

Is there a way to pass the username as a variable to be used as IAM role permission so that it will be able to upload into mybucket/cognito/johnsmith/ instead of this ugly and long mybucket/cognito/us-east-1:c4db3eca-6ed0-af08-4aaa-a33aee2e994a/?

Those IdentityIds are just too long and hard to deal with. I guess I'll have to save IdentityId on my database as well?

Is the information in the token "decodable" or is it just a secure hash. I've noticed that when I refresh only the second half of it changes and when I change the username, the whole token changes.

Please let me know how to setup custom sub-folder with Cognito other than the IdentityId ${cognito-identity.amazonaws.com:sub} I found this article which show that you can use ${aws:username}/* but how do I pass the username into OpenID Token or somewhere else?

Upvotes: 2

Views: 2219

Answers (2)

F_SO_K
F_SO_K

Reputation: 14849

There is no IAM policy variable for the username, but if you wanted to put some effort in I think you could make something work.

  1. Update the Lambda. Use the Cognito SDK to do a ListUsers. You can provide the sub and get the username back. Write the data to a folder with format mybucket/username/sub
  2. Update the bucket policy, putting a wildcard where the username is.

for example:

Resource": ["arn:aws:s3:::mybucket/*/${cognito-identity.amazonaws.com:sub}/*"]

You would still have the sub as a folder name, but it would be under the username folder, so browsing through folder would be easy.

Upvotes: 3

Ninad Gaikwad
Ninad Gaikwad

Reputation: 4480

Unfortunately this isn't possible. The username variable that you see only applies to IAM usernames. An IAM policy cannot read anything from cognito user pool. The sub variable that you see is completely different from the sub that appears in user pool. The sub actually comes from identity pool, which does not have any username. Maybe AWS will allow this in the future, but currently this is not possible.

However, you can simply hide this from users when creating applications. You will only see this big sub variable in bucket path if you log in via console. I have built a similar app where i just show users everything inside this variable as if it is their root folder.

Upvotes: 1

Related Questions