AWS Cognito: How to get the user pool username form an identity ID of an identity pool? - amazon-s3

We allow our users to upload data to a S3 bucket, which then triggers a Python lambda which again updates a DynamoDB entry based on the uploaded file. In the lambda, we struggle to get the username of the user who put the item. Since the Lambda is triggered by the put event from the S3 storage, we don't have the authorizer information available in the request context. The username is required as it needs to be part of the database record.
Here some more background: Every user should only have access to her own files, so we use this IAM policy (created with CDK):
new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
actions: ['s3:PutObject', 's3:GetObject'],
resources: [
bucket.arnForObjects(
'private/${cognito-identity.amazonaws.com:sub}/*'
),
],
})
Since the IAM policy validates the cognito-identity.amazonaws.com:sub field (which translates to the identity ID) we should be able to trust this value. This is the Python lambda and an example of a record we receive:
import json
import boto3
'''
{
"Records": [
{
"eventVersion": "2.1",
"eventSource": "aws:s3",
"awsRegion": "my-region-1",
"eventTime": "2023-02-13T19:50:56.886Z",
"eventName": "ObjectCreated:Put",
"userIdentity": {
"principalId": "AWS:XXXX:CognitoIdentityCredentials"
},
"requestParameters": {
"sourceIPAddress": "XXX"
},
"responseElements": {
"x-amz-request-id": "XXX",
"x-amz-id-2": "XX"
},
"s3": {
"s3SchemaVersion": "1.0",
"configurationId": "XXX",
"bucket": {
"name": "XXX",
"ownerIdentity": {
"principalId": "XXX"
},
"arn": "arn:aws:s3:::XXX"
},
"object": {
"key": "private/my-region-1%00000000-0000-0000-0000-000000000/my-file",
"size": 123,
"eTag": "XXX",
"sequencer": "XXX"
}
}
}
]
}
'''
print('Loading function')
dynamodb = boto3.client('dynamodb')
cognito = boto3.client('cognito-idp')
cognito_id = boto3.client('cognito-identity')
print("Created clients")
def handler(event, context):
# context.identity returns None
print("Received event: " + json.dumps(event, indent=2))
for record in event['Records']:
time = record['eventTime']
key = record['s3']['object']['key']
identityId = key.split('/')[1]
# How to get the username from the identityId?
return "Done"
Things we tried:
Try to find an alternative to cognito-identity.amazonaws.com:sub which validates the username, but from the documentation there is no option for that
Encode the username in the bucket key, but this opens a security hole as then a client can pretend to have a different username
Make a lookup with https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/cognito-identity.html to find the username for an identity ID, but so far we haven't found anything there
Tried to follow How to get user attributes (username, email, etc.) using cognito identity id, but in the Lambda we don't have an ID or access token available
Getting cognito user pool username from cognito identity pool identityId, but since the Lambda is triggered by a S3 put event, we don't have an authorizer context
We could store the identity ID as custom attribute of every Cognito user (as suggested here How to map Cognito (federated) identity ID to Cognito user pool ID?), but before I do that I would like to be sure that there isn't a better option as I fear that the duplication of this information could lead to issues in the long run.

Related

AWS S3 getBucketLogging fails when called from lambda function

I am trying in an AWS lambda to get the bucket logging settings for my buckets. For this I enumerate the buckets with S3.listBuckets() - which works just fine. I then iterate over the bucket names like this (Typescript):
const bucketNames = await getBucketNames() // <- works without problems
for (const bucketName of bucketNames) {
try {
console.log(`get logging for bucket ${bucketName}`) // <-- getting to this log
const bucketLogging: GetBucketLoggingOutput = await s3.getBucketLogging({
Bucket: bucketName,
ExpectedBucketOwner: accountId
}).promise()
// check logging setup and adjust if necessary
} catch (error) {
console.log(JSON.stringify(error))
}
}
The call to getBucketLogging() fails
{
"message": "Access Denied",
"code": "AccessDenied",
"region": null,
"time": "2022-07-19T11:16:26.671Z",
"requestId": "****",
"extendedRequestId": "****",
"statusCode": 403,
"retryable": false,
"retryDelay": 70.19937788683632
}
The accountId that is passed in is definitely right (it's optional anyway); the lambda is in the same account as the bucket owner (which is the sole condition described in the docs at https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/S3.html#getBucketLogging-property).
When doing this call from a terminal CLI I have no problems to get results, only when running from a lambda.
What am I missing or overseeing?
You should make sure to attach the respective IAM permissions to your lambda function. Just because you have the s3:ListBuckets role doesn't mean that it is also permitted to perform the same for the BucketLogging information. Please refer to the following docs for more details on S3 IAM actions: https://docs.aws.amazon.com/service-authorization/latest/reference/list_amazons3.html

Migrating from firebase to auth0

I'm trying to convert from firebase into an auth0 db, by converting firebase export data into the auth0 bulk user import format.
I have a user in firebase (under the firebase_export) section, and the firebase hash config itself (hash config below),
but I'm not clear on how the base64_signer_key fits in or the salt used in the export.
{
"firebase_export": {
"localId": "localId",
"email": "e#ma.il",
"emailVerified": true,
"passwordHash": "base64hash",
"salt": "user_salt",
"lastSignedInAt": "1649680364736",
"createdAt": "1649680237223",
"disabled": false,
"providerUserInfo": []
},
"hash_config": {
"algorithm": "SCRYPT",
"base64_signer_key": "base64_signer_key",
"base64_salt_separator": "base64_salt_separator",
"rounds": 8,
"mem_cost": 14
}
}
I think the schema should look like this, but this is not working.
(I log in to auth0 with a known password and it fails, while passing in firebase).
[
{
"user_id": $localId,
"email": $email,
"email_verified": $emailVerified,
"custom_password_hash": {
"algorithm": "scrypt",
"hash": {
"value": $passwordHash,
"encoding": "base64"
},
"salt" : {
"value": base64Decode($salt) + base64Decode($hash_config.base64_salt_separator),
// based off reading https://github.com/firebase/scrypt
"encoding":"utf8",
"position" "suffix", // based off reading https://github.com/firebase/scrypt, uses PBKDF2_SHA256 which places salt as suffix.
},
"password" : {
"encoding":"utf8"
},
"keylen": 64,
"cost": 2**$hash_config.mem_cost,
"blockSize": $hash_config.rounds,
"parallelization": 1,
},
"blocked": $disabled
}
]
Because Firebase uses a custom bcrypt rather than the standard implementation, auth0 said it is unable to import users.
Other solutions to try:
add a login callback within your code to create/update/delete users in auth0 in an async fashion, to slowly migrate users over.
pay auth0 lots of money to run a custom db migration (still slow).
migrate all users without password and say that we need all users to reset their password
all of which sound suboptimal

How to add mock data containing authorized owner to Amplify (either through GraphiQL or directly to AppSync)?

I'm working on a React app and testing some CRUD functionality by mocking the backend, creating some data through GraphiQL, and running the app (amplify mock, then yarn start).
I want to be able to create mock data tied to my user as the owner because most types in the schema are set up with owner authorization:
type XYZ
#auth(rules: [{ allow: owner, operations: [update, delete, create] }]) {
id: ID!
...more types...etc
}
Right now, I
run amplify mock
Go to GraphiQL local endpoint (192.etc....)
Run some createXYZ mutations to create data
Run my app w yarn start
login with testUser & password
Test the deleteXYZ button which should ideally remove a particular XYZ from the mocked data this is what doesn't work
I suspect what's happening is that I didn't run the createXYZ mutation as testUser, just as a generic GraphiQL user, so the owner property isn't tied to "myUserId". Is that the problem here?
How would I specify owner on my create mutations in GraphiQL?
This is the error I'm getting, pretty sure it means the XYZ object's owner is different than my testUser submitting the deleteXYZ request:
Error while executing Local DynamoDB
{
"version": "2018-05-29",
"operation": "DeleteItem",
"key": {
"id": {
"S": "18b152a6-c98d-4336-be74-1e122191"
}
},
"condition": {
"expression": "( #owner0 = :identity0) AND attribute_exists(#id)",
"expressionNames": {
"#owner0": "owner",
"#id": "id"
},
"expressionValues": {
":identity0": {
"S": "fd2a7758-f7ba-4d57-bdb0-e5346492"
}
}
}
}
Could I have to add the owner id in Amplify's GraphiQL Auth options popup?
I just ran into this issue. I was able to work around it by putting my Cognito User Sub in the username field.

How to send different verification emails at the same cognito user pool

I'm using amazon-cognito for my application user access.
I have two different groups inside my user pool.
I want to send differenet email to each user depends on the group he belongs to.
The problem is that the email verification is sent when the user is created at the pool and not after he's linked to a group.
Is there a way do to it?
Any help? advices?
After a lot of digging, I've figured out a solution.
The solution is to use AWS Cognito Lambda.
Use AWS Cognito Lambda for SignUp or AdminCreateUser events depends on your application architecture.
When a user is created either with SignUp or AdminCreateUser functions, there's an option to pass metadata with clientMetadata entry at the object.
For example (from AWS Docs):
{
"ClientMetadata": {
"string" : "string"
},
"DesiredDeliveryMediums": [ "string" ],
"ForceAliasCreation": boolean,
"MessageAction": "string",
"TemporaryPassword": "string",
"UserAttributes": [
{
"Name": "string",
"Value": "string"
}
],
"Username": "string",
"UserPoolId": "string",
"ValidationData": [
{
"Name": "string",
"Value": "string"
}
]
}
According to docs:
clientMetadata
One or more key-value pairs that you can provide as
custom input to the Lambda function that you specify for the pre
sign-up trigger. You can pass this data to your Lambda function by
using the ClientMetadata parameter in the following API actions:
AdminCreateUser, AdminRespondToAuthChallenge, ForgotPassword, and
SignUp.
So, pass the group inside the clientMetadata as an entry:
"ClientMetadata": {
"Group": "MyNiceGroup"
},
...
Inside the lambda implementation, according to the incoming group decide which email to dispatch.

Extracting custom objects from HttpContext

Context
I am rewriting an ASP.NET Core application from being ran on lambda to run on an ECS Container. Lambda supports the claims injected from Cognito Authorizer out of the box, but Kestrel doesn't.
API requests are coming in through API Gateway, where a Cognito User Pool authorizer is validating the OAuth2 tokens and enriching the claims from the token to the httpContext.
Originally the app was running on lambda where the entry point was inheriting Amazon.Lambda.AspNetCoreServer.APIGatewayProxyFunction, which extracts those claims and adds them to Request.HttpContext.User.Claims.
Kestrel of course doesn't support that and AWS ASPNET Cognito Identity Provider seems to be meant for performing the same things that the authorizer is doing.
Solution?
So I got the idea that maybe I can add some custom code to extract it. The HTTP request injected into lambda looks like this, so I expect it should be the same when it's proxied into ECS
{
"resource": "/{proxy+}",
"path": "/api/authtest",
"httpMethod": "GET",
"headers": {
<...>
},
"queryStringParameters": null,
"pathParameters": {
"proxy": "api/authtest"
},
"requestContext": {
"resourceId": "8gffya",
"authorizer": {
"cognito:groups": "Admin",
"phone_number_verified": "true",
"cognito:username": "normj",
"aud": "3mushfc8sgm8uoacvif5vhkt49",
"event_id": "75760f58-f984-11e7-8d4a-2389efc50d68",
"token_use": "id",
"auth_time": "1515973296",
"you_are_special": "true"
}
<...>
}
Is it possible, and how could I go about it to add all the key / value pairs from requestContext.authorizer to Request.HttpContext.User.Claims?
I found a different solution for this.
Instead of trying to modify the HttpContext I map the authorizer output to request headers in the API Gateway integration. Downside of this is that each claim needs to be hardcoded as it doesn't seem to be possible to iterate over them.
Example terraform
resource "aws_api_gateway_integration" "integration" {
rest_api_id = "${var.aws_apigateway-id}"
resource_id = "${aws_api_gateway_resource.proxyresource.id}"
http_method = "${aws_api_gateway_method.method.http_method}"
integration_http_method = "ANY"
type = "HTTP_PROXY"
uri = "http://${aws_lb.nlb.dns_name}/{proxy}"
connection_type = "VPC_LINK"
connection_id = "${aws_api_gateway_vpc_link.is_vpc_link.id}"
request_parameters = {
"integration.request.path.proxy" = "method.request.path.proxy"
"integration.request.header.Authorizer-ResourceId" = "context.authorizer.resourceId"
"integration.request.header.Authorizer-ResourceName" = "context.authorizer.resourceName"
"integration.request.header.Authorizer-Scopes" = "context.authorizer.scopes"
"integration.request.header.Authorizer-TokenType" = "context.authorizer.tokenType"
}
}