Preventing malicious actions in dynamo DB accessed via lambda over REST - amazon-web-services

Situation: There is a dynamo DB containing a column with the username, one with a unique ID, and the data for each post submitted via an Angular frontend (REST) that triggers the lambda function over AWS API gateway.
Angular frontend --> AWS API Gateway (authenticate) --> Lambda function (write to) --> DynamoDB
Challenge: While only allowing authenticated requests to the API there are not user details (only the token)
'identity': {'cognitoIdentityPoolId': None, 'accountId': None, 'cognitoIdentityId': None, 'caller': None}
according to the AWS documentation, there is no way to obtain the user who has triggered the lambda function. Therefore currently I have to rely on the value provided via Angular (which can be manipulated)
I want to ensure that no evil user alters the username (provided by the frontend) by submitting HTTP posts with postman or another tool and therefore overrides or creates entries on behalf of another user. Or any other idea, how to create records in a table (via lambda) in a way that the primary key goes to a particular user, while preventing other authenticated users to submit requests on his behalf

It turns out, that the amplify SignUp API generates a persistent UUID for a user, and uses it as the immutable username attribute internally. This UUID has the same value as the sub claim in the user identity token.
Therefore I am now using:
let userID = (await Auth.currentUserInfo()).attributes.sub;
in the frontend, as the UUID is not predictable, and require the ID for all reads and writes.
See: https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-settings-attributes.html#user-pool-settings-custom-attributes.html

Related

what is the best practise to store user information obtained from cognito authentication in dynamodb

In frontend, I will do the registration and login using cognito.
Let's assume I have a "User" table to store user information.
Username(using cognito username) will be the partition key in the table.
Every user has different permissions which would be store as list of strings, such as ["admin", "viewer"] etc, so there will be an attribute called "permissions"
There is the registration and login flow I am considering:
Registration in frontend > Cognito registration(await Auth.signUp) > if successful, store user information in dynamodb by using lambda function.
The request body of POST request to dynamodb will like this.
{
username: xxxxx (partition key),
permissions: ["admin", "viewer"],
isConfirmed: 0
}
2.If user is confirmed in Cognito User Pool, send PATCH request to change isConfirmed to 1
3.user login in frontend > Cognito login(await Auth.signIn) > query for user data by username in dynamodb
A few questions below.
a)Is there any problems in registration and login flow that I can improve?
b)for (2), can I trigger the lambda function if the confirmation is done?
Check out the documentation about Cognito triggers. You could use the post confirmation trigger to execute a lambda once your user passes the confirmation step of your sign-up process. This would let you avoid the 2nd step of your proposed solution (sending a PATCH request) and just worry about the logic of storing user info in the database.

Should you use Client Credentials Grant Type with authenticating server-to-server?

I'm creating an api service ("My Api") where the end users are other apis ("Client"). This is my first application where the Client is not an actual person, so I want to make sure I'm going through the authentication flow correctly.
I'm using AWS Cognito and have based the authentication flow off the "Client credentials grant" section of this post.
The flow I have right now is:
Client registers with My Api
My Api creates an app client on AWS. I have a simple dashboard that will display the client_id and client_secret to the Client (My Api exposes an endpoint to rotate client_secrets)
Client sends the following POST to my AWS oauth2 domain
curl -X POST \
https://[DOMAIN_NAME].auth.[REGION].amazoncognito.com/oauth2/token \
-H 'Content-Type: application/x-www-form-urlencoded' \
-H 'authorization: Basic BASE64(client_id:client_secret)' \
-d 'grant_type=client_credentials&scope=[SCOPE]'
Client receives access_token in the form of a jwt from AWS
Client sends access_token in authorization header to My Api
My Api verifies the access_token is valid
My Api provides access to resources for the applicable scope and client_id
It seems strange that I have to create an app client on AWS Cognito for each Client. Is that normal when you're authenticating using client credentials instead of an authorization code?
If that's the case, can someone direct me to what the pricing is for each each app client? Is it in the "Users who sign in directly with their User Pool credentials or with social identity providers:" section on this page?
After some time to think about this, this is what I would do (preface: this is definitely not AWS/Banking level authentication). The code below is in postgres.
I would design the database schema to accommodate multiple tenants see this paper by Google for ideas. Each User (eg, employee of an Organization) will have a Cognito User which will be linked to the User.
CREATE TABLE organizations (
org_id uuid
);
CREATE TABLE users (
user_id uuid,
cognito_uid uuid,
org_id uuid REFERENCES organizations(org_id)
);
CREATE TABLE secret_stuffs (
secrets varchar
);
I would then create an api_keys table.
// We only want user to have two keys max
CREATE TYPE api_key_type AS ENUM (
'primary',
'secondary'
);
CREATE TABLE api_keys (
PRIMARY KEY (
user_id,
key_type
)
org_id uuid REFERENCES organizations(org_id),
user_id uuid REFERENCES users(user_id),
key_type api_key_type,
private_key varchar
);
// You'd probably want to create a composite index with user_id and private_key fields since we'll create a function that access both
I would lock down the api_keys and secret_stuffs table (ie, not grant access to any role) and create a SECURITY DEFINER function that takes the user_id and private_key as inputs, checks that that row exists in your api_keys table and returns whatever you need from the secret_stuffs table.
I had the same challenge months ago when we had to authenticate requests from server side applications. And based on my research, Implicit flow and authorization code flow is applicable for Front-End login authentication, and Client Credentials flow is for machine-to-machine. I also setup a separate Database to map the the Client App ID for each of the server side applications that we plan to authenticate.
I found this tutorial on how to use OAuth 2.0 Client credentials Flow. It was discussed in good details with example on how to test in postman.
Authorization: Basic BASE64(CLIENT_ID:CLIENT_SECRET)

AWS Cognito - run another lambda after migration lambda has run

Cognito has a migration lambda that allows us to confirm a user in our db. They send the email and PW to Cognito, the lambda fires, we verify matches, and the user is entered into Cognito.
At this point - behind the scenes - Cognito generates a username of some kind (UUID). The problem is, I need a way to get this username into our existing database, because our systems going forward will no longer rely on email and instead rely on this username.
Ideal flow:
Sign In
Migration Succeeds
Cognito generates username
Username is sent to our server.
Now because we have email set to auto-verified, no post-confirmation lambda can be called. The only way I see to do this with Cognito as-is is to either:
Ask users who already exist in our system to confirm their email again. This is a non-starter
Create a post-auth lambda, check user login count through a custom attribute, and if 0 (or if not already registered with the service, etc.) migrate the username to the new service.
If there is any other way to do this, please let me know.
After the user migration lambda is called your pre sign-up lambda will be called, assuming you have implemented it. The parameters received by your lambda will include username with the value being the UID you referenced. Parameters will also include user attributes containing email. You can use this information to update your database.
I did not want to add the PreSignup trigger, its a complicated way of doing it if you already rely on PostConfirmation, and if the majority of new users won't be migrations. My use case has a frontend initiate the signup process as well, which I use here.
Instead, I set a Cognito attribute on the new user during the UserMigration trigger. It could be 'user_migration': <oldUserSub>, or however you want to mark it. Just make sure you allow this property within the Cognito user pool settings.
When the UserMigration trigger returns, this information is now accessible through verifying the IdToken, or found in the JWT on the frontend if you're using that. So, when the user is migrated into Cognito and the response gets back to the Cognito client on the frontend, I can now recognize this user needs to be migrated into my personal database. Seeing this, I'll call a new endpoint on my backend to handle this. This new endpoint does exactly what PostConfirmation would typically do.
Then just delete the 'user_migration' property from the Cognito user, return the new user data to the frontend and everything should be set up.
You can use Pre sign-up trigger. In order to detect if the trigger event came from your migration trigger, you can check at the trigger_source value from the event object. In my case (i'm using migration trigger) the value is PreSignUp_AdminCreateUser. By knowing the value of trigger_source you can differentiate if it was migrated or regular user. You can also check the user attributes to know whether the email or phone is verified or not.
Here's my sample code on python:
def lambda_handler(event, context):
trigger_source = event.get('triggerSource')
user_attributes = request.get('userAttributes')
email_verified = user_attributes.get('email_verified')
if trigger_source == 'PreSignUp_AdminCreateUser' and email_verified == 'true':
# create user on db

AWS Cognito custom auth - sending metadata to a challenge lambda functions

I'm developing a custom passwordless auth to sign into a Cognito user pool. I will describe what I'm trying to implement in case anything is silly. I want a user to enter their email address, then receive a magic login link via email, and when they click on that be taken back to the site and be logged in.
This uses custom auth lambda functions to define/create a challenge with a time based password and send it to the user in an email. I am having a couple of problems:
Problem 1)
When the user returns with the code they might not be in the same browser/device and certainly won't be in the same tab so they don't have the session, meaning I need to call cognitoUser.initiateAuth again. This goes through the define/create challenge lambdas again so a second email gets sent even though at this point the user is coming from the email link so already has the code. Note: the session id is not available in the event object when the challenge is created, also I've read these sessions only last 3 minutes and my time based passwords will last ~15minutes, so I don't think I can include the session id in the email.
Problem 2)
You can login from a few places (browser, android app, etc) and I would like to be able to include the url or at least protocol as a parameter to control what gets sent in the email, e.g. if you entered your email address in the android app then the email you get would be myapp://login?code=xxx and if you did it on the web it would be https://example.com/login?code=xxx
It seems like I would be able to implement both of these to work properly if only I could find some way to send custom metadata through to the DefineChallenge and CreateChallenge lambda such that it would appear in the event object. I thought adding ValidationData to the AuthenticationDetails object would do this, but that information doesn't appear in the event object in the Lambda fns.
The workaround I've found is to create a new client id for every situation - one for initiating auth, one for redeeming token, and repeat for each different protocol. But that is a lot of client ids quickly - a pain to mantain and clumsy.
So tl;dr is: I want to send custom metadata from my cognitoUser.initiateAuth(...) call in JS and have it available in my Define/Create Challenge lambda fns.
You can split the authentication process into multiple custom auth challenges. This allows custom auth state to be supplied via the challenge response as client metadata.
Auth session state must be persisted in a database in order to be shared between devices.
Your custom login flow will probably have two challenge steps: the first prompts for auth type and the second prompts for the secret code. The action taken by the "Create Auth Challenge" Lambda will depend on the auth type. If the auth type is "Email" then the secret code and magic link are generated, stored in DynamoDB and emailed. If the auth type is "MagicLink" then the secret is loaded from DynamoDB. Clicking on a Magic link will initiate a new auth session and automatically supply all the challenge answers.
There are a few other things to consider:
Your magic link needs to encapsulate the Cognito username as well as the one-time secret and probably some other session id that is used as a key in dynamodb.
You probably should not put app-specific links into your emails. Instead associate your domain with your app and/or leverage the redirect URI parameter of your web-based login page.
You can also access custom Cognito user attributes from the Lambda function which can be used to indicate user login preferences (eg Email vs SMS for login codes).

Save AWS Cognito Users in DynamoDB

I recently started experimenting with AWS AppSync but I had some questions around AWS Cognito.
I would like for users to be able to authenticate with Facebook but I need their profile picture, name and email as data for my public user profiles in my app. So far, I noticed Cognito integrates with Facebook Auth but it does not allow access to the user information and this info does not get saved in a DynamoDB table.
My question is, how can I create a new User in DynamoDB when Cognito receives a new sign in, or return an existing user/id when the user already exists in the db.
I was trying to achieve the same a few weeks ago.
After reading the docs for hours, I realised that Cognito may not help us in regards to the data that comes back from FB or how to save it.
I ended up doing the following:
(1) Using FB-SDK, pulled in the user data.
(2) Invoked a Lambda function that saved this data (like FB_id,etc) to DynamoDB.
(3) If user logged in again, their FB_id (or email) was used to check against DynamoDB entries to retrieve their data.
If Cognito is able to help us and I missed it somehow, I would love to know.
Happy Coding!
You could use custom attributes and federating user from Facebook in your user pool to achieve this. Here are the steps at high level to do this.
You will first have to define custom attributes for the profile information you want to save in each user profile.
Define attribute mapping to link the custom attributes to Facebook attributes you want to save.
Build you application using Cognito hosted pages and federation to allow your users to log in using Facebook.
After this, on each new user log in in your app a new user is created in your user pool with all the attributes that were defined in attribute mapping and values which Cognito gets in the Facebook token. Your app will get these attribute values in the IDToken issued after authentication and you app can use these.
Additionally, if you want to store these attribute values outside of Cognito user pools profile, like your own DynamoDB table, you can configure a PreSignUp trigger in the pool which will be invoked on all new user creations. You can export the user attributes from this trigger to any database of your choice.
Hope this helps.
AWS AppSync allows you to access information in the GraphQL resolver which you can choose to store in a DynamoDB table. In your case for data coming from a Facebook profile you could pass this as arguments to a GraphQL mutation or in a header to AppSync which you can then access in the resolver via $ctx.request.headers.NAME where NAME is your header name. Then you could simply choose which attributes you want to write to DynamoDB for that user as part of the mutation. More information is in the reference guide here: https://docs.aws.amazon.com/appsync/latest/devguide/resolver-context-reference.html
Since you also asked that you'd like to do a check first to see if the user is already in the DDB first you could just do an existence check first:
{
"version": "2017-02-28",
"operation": "PutItem",
"key": {
"userId": $util.dynamodb.toDynamoDBJson($ctx.identity.username),
},
"attributeValues": $util.dynamodb.toMapValuesJson($ctx.args.input),
"condition": {
"expression": "attribute_not_exists(userId)"
},
}
This checks against the username from Cognito User Pools. If you were using the Cognito Federated Identities feature it would be ctx.identity.cognitoIdentityId. If the record is already there the response that comes back will tell you which means the user is already present. You could also transform the returned message in the response mapping template by looking at $ctx.result with a conditional statement and either building the JSON response by scratch or using one of the $util.error() methods in the guide above.
Finally as you mentioned that you'll have public profile data, you might want to mark this on certain records for control. In AWS AppSync you can filter GraphQL responses on authorization metadata such as this. You would just have an attribute (aka column) on the DynamoDB record marked 'public' or 'private. Then your response template would look like so:
#if($context.result.public == 'yes')
$utils.toJson($context.result)
#else
$utils.unauthorized()
#end
You can see more examples of this here: https://docs.aws.amazon.com/appsync/latest/devguide/security-authorization-use-cases.html#public-and-private-records