I have set up a small iOS app using AWS Cognito to sign up and sign in users. It is now working. Next I want the users to be able to log in and then set or change their family name (as well as first name and maybe some other information) in the Cognito database.
For that I presume I will need to create a lambda function and use it to update the family_name and given_name attibutes (at least this should be one solution).
But what I have tried is not yet working. This is what I have at this point.
The code of the lambda function (this is obviously testing code, but it should work):
var AWS = require('aws-sdk');
exports.handler = async (event,context) => {
var cognitIdSP = new AWS.CognitoIdentityServiceProvider();
var params = {
UserAttributes: [
{
Name: 'family_name',
Value: 'Kennedy'
},
{
Name: 'given_name',
Value: 'John_Fitzerald'
},
],
UserPoolId: 'ap-northeast-1_xxyyzz',
Username: 'zob'
};
cognitIdSP.adminUpdateUserAttributes(params, function(err, data) {
if (err) console.log(err, err.stack); // an error occurred
else console.log(data); // successful response
});
}
And the policy for the execution role of the lambda function above is:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": "cognito-idp:AdminUpdateUserAttributes",
"Resource": "arn:aws:cognito-idp:ap-northeast-1:123456789:userpool/ ap-northeast-1_xxyyzz"
}
]
}
As far as I can see, no attribute is updated in the Cognito database when I run this function.
I have something like this - working fine:
const AWS = require('aws-sdk');
const cognitoidentityserviceprovider = new AWS.CognitoIdentityServiceProvider();
exports.handler = async (event, context) => {
const emailVerified = await cognitoidentityserviceprovider
.adminUpdateUserAttributes({
UserAttributes: [{
Name: 'family_name',
Value: 'name'
}],
UserPoolId: event.userPoolId,
Username: event.userName
})
.promise();
context.done(null, event);
};
Related
I am beginner, I found two instructions on the internet to get data ( Contact us - Email, Phone, etc ) to AWS - Lambda then send to my email
https://aws.amazon.com/blogs/architecture/create-dynamic-contact-forms-for-s3-static-websites-using-aws-lambda-amazon-api-gateway-and-amazon-ses/
And another one to make a AWS - Lambda function to send to Dynamo to back up.
https://www.vairix.com/tech-blog/get-and-put-data-using-lambda-and-dynamodb-simple-and-clear
I put them together, but they don't connect. For example, Lamba trigger to send info from website to my email but the other function wont collect the item to send it to Dynamo
Lambda to send email:
var AWS = require('aws-sdk');
var ses = new AWS.SES();
var RECEIVER = 'contactus#example.com';
var SENDER = 'contactus#example.com';
var response = {
"isBase64Encoded": false,
"headers": { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': 'example.com'},
"statusCode": 200,
"body": "{\"result\": \"Success.\"}"
};
exports.handler = function (event, context) {
console.log('Received event:', event);
sendEmail(event, function (err, data) {
context.done(err, null);
});
};
function sendEmail (event, done) {
var params = {
Destination: {
ToAddresses: [
RECEIVER
]
},
Message: {
Body: {
Text: {
Data: 'name: ' + event.name + '\nphone: ' + event.phone + '\nemail: ' + event.email + '\ndesc: ' + event.desc,
Charset: 'UTF-8'
}
},
Subject: {
Data: 'Website Referral Form: ' + event.name,
Charset: 'UTF-8'
}
},
Source: SENDER
};
ses.sendEmail(params, done);
}
Lambda to store information in DynamoDB:
"use strict";
AWS.config.update({ region: "us-east-1" });
exports.handler = async() => {
const documentCilent = new AWS.DynamoDB.DocumentClient({region: "us-east-1"});
const params = {
TableName: "Users",
// This part, I need the variable connect with the top part.
Item:{
id : "test",
name: "name",
phone: "phone",
desc: "desc",
email: "email"
}
};
try {
const data = await documentCilent.put(params).promise();
console.log(data);
} catch (err) {
console.log(err);
}
};
Lambda's are independent than each other and only fire upon receiving an Event to do so. This Event can be several things - It can be an Event sent to the lambda from an API Gateway (this is what connecting your Api to your Lambda does - gives a target for the Api Event to go to). It can be an event generated by your SDK (using your languases SDK to invoke another lambda). It can be from a DynamoDB stream or an S3 put_object event.
Pretty much any thing that happens in AWS resources outputs an event of some kind, and with the right commands/setups you can have a Lambda listening for that event.
In this situation however, as these are both very light weight, it would just be more cost effective to combine them under one lambda. API gets the information, sends it to the lambda which fires an email and adds it to the Dynamodb. Anything else in this situation is kinda overkill and would increase your costs.
I used Amplify Cli to create the templated api 'amplify add api' with new lambda function and cognito authentication. This is the code generated in the index.js file of the lambda function:
/************************************
index.js
*************************************/
const awsServerlessExpress = require('aws-serverless-express');
const app = require('./app');
const server = awsServerlessExpress.createServer(app);
exports.handler = (event, context) => {
console.log(`EVENT: ${JSON.stringify(event)}`);
awsServerlessExpress.proxy(server, event, context);
};
/************************************
HTTP put method for insert object in app.js
*************************************/
app.put(path, function(req, res) {
if (userIdPresent) {
req.body['userId'] = req.apiGateway.event.requestContext.identity.cognitoIdentityId || UNAUTH;
} else {
// Get the unique ID given by cognito for this user, it is passed to lambda as part of a large string in event.requestContext.identity.cognitoAuthenticationProvider
let userSub = req.apiGateway.event.requestContext.identity.cognitoAuthenticationProvider.split(':CognitoSignIn:')[1];
let requestIp = req.apiGateway.event.requestContext.identity.sourceIp;
let userPoolId = process.env.AUTH_TDOCWEBAPP001_USERPOOLID;
let request = {
UserPoolId: userPoolId, // Set your cognito user pool id
AttributesToGet: [
'email',
'given_name',
'family_name',
],
Filter: 'sub = "' + userSub + '"',
Limit: 1
}
//let users = await cognitoClient.listUsers(request).promise(); //Doesn't work because await not allowed
let users = cognitoClient.listUsers(request);
console.log("got user in put:", users[0]);
// TODO: Get the group that the user belongs to with "cognito:grouops"?
// Set userId and sortKey
req.body['userId'] = users[0].sub;
req.body['sortKey'] = sortKeyValue;
req.body['updatedByIp'] = requestIp;
req.body['createdAt'] = new Date().toISOString(); //ISO 8601 suppored by DynamoDB
req.body['updatedAt'] = new Date().toISOString();
req.body['isDeleted'] = false;
}
let putItemParams = {
TableName: tableName,
Item: req.body
}
dynamodb.put(putItemParams, (err, data) => {
if(err) {
res.statusCode = 500;
res.json({error: err, url: req.url, body: req.body});
} else{
res.json({success: 'put call succeed!', url: req.url, data: data})
}
});
});
So right now, when I call the lambda via the API, i get users is undefined. I'm trying to get the user object and then the groups that it belongs. Not sure how to do it if the function doesn't allow async... Please help
I found it myself the solution.
let users = await cognitoClient.listUsers(request);
or
let users = cognitoClient.listUsers(request, function(err, data) {...});
I needed wait to get users from the Cognito.
I am working with serverless and NodeJS using the aws-sdk module, and I have been getting a 502 internal server error on the frontend of the app, previously it was shown as a cors error but so far it is the same issue in only one specific table when performing update or delete. Here is the Lambda function itself:
export async function createUserSegment(event, context, callback) {
const { data, timestamp, identityId } = extractRequestData(event)
console.log('data', JSON.stringify(data));
if (!verifyParamsExist(callback, ["Data", "IdentityId"], [data, identityId]) ||
!verifyParamsExist(callback, ["Name", "Pattern", "Type"], [data.name, data.type, data.pattern]) ||
!verifyValidParams(callback, [data.name, ...data.pattern], ["Name", "Pattern"], [{ value: data.type, enums: USER_SEGMENT_TYPES }], ["Type"])) {
console.log('no good params');
return false
}
let cleanedParams
let params
try {
cleanedParams = cleanUserSegment(data.pattern, data.type)
console.log('cleaned params', cleanedParams);
} catch (e) {
console.error(e)
callback(null, badRequest({ type: "InvalidRule", status: false, msg: e.message }));
return
}
params = {
TableName: process.env.USER_SEGMENT_TABLE,
Key: {
"cognitoUsername": fetchSubFromEvent(event),
"segmentName": data.name.trim(),
},
// Performs an insert (fails if already exists)
// - 'cognitoUsername': cognito username (NOT IDENTITY ID!) of the segment creator.
// - 'segmentName': user-provided name to identify the segment
// - 'createdAt': unix timestamp for when segment is created
// - 'pattern': user-supplied pattern that the segment refers to
// - 'segmentType': either VISITED, NOT_VISITED, GOAL, NOT_GOAL to indicate what our pattern is matching (URL visits or goal completion)
// - 'identityId': cognito identity id of the segment creator. necessary for querying ES
UpdateExpression: 'SET pattern = :pattern, segmentType = :segmentType, createdAt = :createdAt, identityId = :identityId',
ConditionExpression: 'attribute_not_exists(cognitoUsername)',
ExpressionAttributeValues: {
":pattern": cleanedParams.pattern,
":segmentType": cleanedParams.type,
":createdAt": timestamp,
":identityId": identityId,
},
ReturnValues: "ALL_NEW"
};
writeToDynamoAndCatchDuplicates(params, callback, transformUserSegment)
}
async function writeToDynamoAndCatchDuplicates(params, callback, transformFn){
try {
if(params.ConditionExpression){
console.log(params);
let result = await dynamoDbLib.call("update", params);
console.log('this is res',result);
result.Attributes = transformFn(result.Attributes)
callback(null, success({ status: true, result: result }));
} else {
await dynamoDbLib.call("update", params);
const paramsForAll = {
TableName: params.TableName,
KeyConditionExpression: "#cognitoUsername = :username",
ExpressionAttributeNames: {
"#cognitoUsername": "cognitoUsername",
},
ExpressionAttributeValues: {
":username": params.Key.cognitoUsername
},
}
try {
let result = await dynamoDbLib.call("query", paramsForAll);
result.Items = result.Items.map(transformFn)
console.log(result);
callback(null, success({ status: true, result: result}));
} catch (e) {
console.error(e)
callback(null, failure({ status: false }));
}
}
} catch (e) {
console.error(e)
if(e.code === 'ConditionalCheckFailedException'){
callback(null, badRequest({ type: "Duplicate", status: false }));
} else {
callback(null, failure({ status: false }));
}
}
}
The call function for dynamoDbLib is simply:
import AWS from "aws-sdk";
AWS.config.update({ region: "us-east-1" });
export function call(action, params) {
const dynamoDb = new AWS.DynamoDB.DocumentClient();
return dynamoDb[action](params).promise();
}
The request goes through the Lambda up until within writeToDynamoAndCatchDuplicates the call function is made. it gets sent to dynamo but it never returns, neither a timeout nor an error, it simply ends. In cloudwatch the last thing I see is the params log from writeToDynamoAndCatchDuplicates, no error logged. I hadn't changed the IAM policies on the role assigned to all Lambdas with the following permissions for the table aforementioned:
...},
{
"Action": [
"dynamodb:UpdateItem",
"dynamodb:Query",
"dynamodb:Scan",
"dynamodb:DeleteItem"
],
"Resource": [
"arn:aws:dynamodb:us-east-1:xxxxxxxxxxxxx:table/staging-userSegmentTable"
],
"Effect": "Allow"
},
{ ...
If anyone spots anything that might be a possible reason for the issue, it will be much appreciated.
Also here is the last log shown before the END message on LOG events:
2020-01-07T23:42:03.715Z 4fbf0957-c8be-4520-8277-7b9a9bda8b67 INFO { TableName: 'staging-userSegmentTable',
Key:
{ cognitoUsername: '*******************************',
segmentName: 'cssfs' },
UpdateExpression:
'SET pattern = :pattern, segmentType = :segmentType, createdAt = :createdAt, identityId = :identityId',
ConditionExpression: 'attribute_not_exists(cognitoUsername)',
ExpressionAttributeValues:
{ ':pattern': [ '/' ],
':segmentType': [ 'CONTAINS' ],
':createdAt': 1578440523709,
':identityId': '****************************************' },
ReturnValues: 'ALL_NEW' }
which corresponds to console.log(params) before sending to DynamoDB inside the if statement of the writeToDynamoAndCatchDuplicates function.
Does your IAM have Read and Write Access to that Database and security group connected to the instance? Is this the only table that seems to be the issue?
I had a similar issue ended up having to give my DB instance full read access.
I am building a social chat application and initially had a cognito user pool that was federated alongside Google/Facebook. I was storing user data based on the user-sub for cognito users and the identity id for google/facebook. Then in my lambda-gql resolvers, I would authenticate via the AWS-sdk:
AWS.config.credentials = new AWS.CognitoIdentityCredentials({
IdentityPoolId: process.env.IDENTITY_POOL_ID,
Logins: {
[`cognito-idp.us-east-1.amazonaws.com/${
process.env.COGNITO_USERPOOL_ID
}`]: Authorization,
},
});
Because all users are equal and I don't need fine grained controls over access to aws-resources, it seems like it would be preferable to instead have all authentication handled via the userpool and to get rid of the identity pool entirely.
For example, if I wanted to ban a user's account, it seems that I would first have to lookup the provider based on identity-id and then perform a different action based on the provider.
So my questions are:
1. Is this even possible?
- https://github.com/aws-amplify/amplify-js/issues/565
-https://www.reddit.com/r/aws/comments/92ye5s/is_it_possible_to_add_googlefacebook_user_to/
There seems to be a lot of confusion, and the aws docs are less clear than usual (which isn't much imo).
https://docs.aws.amazon.com/cognito/latest/developerguide/authentication.html
It seems that there is clearly a method to do this. I followed the above guide and am getting errors with the hosted UI endpoint, but that's probably on me (https://forums.aws.amazon.com/thread.jspa?threadID=262736). However, I do not want the hosted UI endpoint, I would like cognito users to sign in through my custom form and then social sign in users to click a "continue with fb" button and have that automatically populate my userpool.
Then replace the code above with the following to validate all users:
const validate = token => new Promise(async (resolve) => {
const {
data: { keys },
} = await axios(url);
const { sub, ...res } = decode(token, { complete: true });
const { kid } = decode(token, { header: true });
const jwk = R.find(R.propEq('kid', kid))(keys);
const pem = jwkToPem(jwk);
const response = res && res['cognito:username']
? { sub, user: res['cognito:username'] }
: { sub };
try {
await verify(token, pem);
resolve(response);
} catch (error) {
resolve(false);
}
});
If it is possible, what is the correct mechanism that would replace the following:
Auth.federatedSignIn('facebook', { token: accessToken, expires_at }, user)
.then(credentials => Auth.currentAuthenticatedUser())
.then((user) => {
onStateChange('signedIn', {});
})
.catch((e) => {
console.log(e);
});
From what I have seen, there does not appear to be a method with Amplify to accomplish this. Is there some way to do this with the aws-sdk? What about mapping the callback from the facebook api to create a cognito user client-side? It seems like that could get quite messy.
If there is no mechanism to accomplish the above, should I federate cognito users with social sign ins?
And then what should I use to identify users in my database? Am currently using username and sub for cognito and identity id for federated users. Extracting the sub from the Auth token server-side and then on the client:
Auth.currentSession()
.then((data) => {
const userSub = R.path(['accessToken', 'payload', 'sub'], data);
resolve(userSub);
})
.catch(async () => {
try {
const result = await Auth.currentCredentials();
const credentials = Auth.essentialCredentials(result);
resolve(removeRegionFromId(credentials.identityId));
} catch (error) {
resolve(false);
}
});
If anyone could provide the detailed authoritative answer I have yet to find concerning the use of cognito user pools in place of federating that would be great. Otherwise a general outline of the correct approach to take would be much appreciated.
Here's what I ended up doing for anyone in a similar position, this isn't comprehensive:
Create a userpool, do not specify client secret or any required attributes that could conflict with whats returned from Facebook/Google.
Under domains, in the Cognito sidebar, add what ever you want yours to be.
The add your identity provided from Cognito, for FB you want them to be comma seperated like so: openid, phone, email, profile, aws.cognito.signin.user.admin
Enable FB from app client settings, select implicit grant. I belive, but am not positive, openid is required for generating a access key and signin.user.admin for getting a RS256 token to verify with the public key.
The from FB dev console, https://yourdomain.auth.us-east-1.amazoncognito.com/oauth2/idpresponse, as valid oauth redirects.
Then, still on FB, go to settings (general not app specific), and enter https://yourdomain.auth.us-east-1.amazoncognito.com/oauth2/idpresponse
https://yourdomain.auth.us-east-1.amazoncognito.com/oauth2/idpresponse for your site url.
Then for the login in button you can add the following code,
const authenticate = callbackFn => () => {
const domain = process.env.COGNITO_APP_DOMAIN;
const clientId = process.env.COGNITO_USERPOOL_CLIENT_ID;
const type = 'token';
const scope = 'openid phone email profile aws.cognito.signin.user.admin';
const verification = generateVerification();
const provider = 'Facebook';
const callback = `${window.location.protocol}//${
window.location.host
}/callback`;
const url = `${domain}/authorize?identity_provider=${provider}&response_type=${type}&client_id=${clientId}&redirect_uri=${callback}&state=${verification}&scope=${scope}`;
window.open(url, '_self');
};
Then on your redirect page:
useEffect(() => {
// eslint-disable-next-line no-undef
if (window.location.href.includes('#access_token')) {
const callback = () => history.push('/');
newAuthUser(callback);
}
}, []);
/* eslint-disable no-undef */
import { CognitoAuth } from 'amazon-cognito-auth-js';
import setToast from './setToast';
export default (callback) => {
const AppWebDomain = process.env.COGNITO_APP_DOMAIN;
// https://yourdomainhere.auth.us-east-1.amazoncognito.com'
const TokenScopesArray = [
'phone',
'email',
'profile',
'openid',
'aws.cognito.signin.user.admin',
];
const redirect = 'http://localhost:8080/auth';
const authData = {
ClientId: process.env.COGNITO_USERPOOL_CLIENT_ID,
AppWebDomain,
TokenScopesArray,
RedirectUriSignIn: redirect,
RedirectUriSignOut: redirect,
IdentityProvider: 'Facebook',
UserPoolId: process.env.COGNITO_USERPOOL_ID,
AdvancedSecurityDataCollectionFlag: true,
};
const auth = new CognitoAuth(authData);
auth.userhandler = {
onSuccess() {
setToast('logged-in');
callback();
},
onFailure(error) {
setToast('auth-error', error);
callback();
},
};
const curUrl = window.location.href;
auth.parseCognitoWebResponse(curUrl);
};
You can then use Auth.currentSession() to get user attributes from the client.
Then server-side you can validate all user like so:
const decode = require('jwt-decode');
const jwt = require('jsonwebtoken');
const jwkToPem = require('jwk-to-pem');
const axios = require('axios');
const R = require('ramda');
const logger = require('./logger');
const url = `https://cognito-idp.us-east-1.amazonaws.com/${
process.env.COGNITO_USERPOOL_ID
}/.well-known/jwks.json`;
const verify = (token, n) => new Promise((resolve, reject) => {
jwt.verify(token, n, { algorithms: ['RS256'] }, (err, decoded) => {
if (err) {
reject(new Error('invalid_token', err));
} else {
resolve(decoded);
}
});
});
const validate = token => new Promise(async (resolve) => {
const {
data: { keys },
} = await axios(url);
const { sub, ...res } = decode(token, { complete: true });
const { kid } = decode(token, { header: true });
const jwk = R.find(R.propEq('kid', kid))(keys);
const pem = jwkToPem(jwk);
const response = res && res['cognito:username']
? { sub, user: res['cognito:username'] }
: { sub };
try {
await verify(token, pem);
resolve(response);
} catch (error) {
logger['on-failure']('CHECK_CREDENTIALS', error);
resolve(false);
}
});
const checkCredentialsCognito = Authorization => validate(Authorization);
I am attempting to create a Cognito user pool programmatically from a script using the JavaScript SDK.
I have successfully created the user-pool and defined a pre-signup and post-confirmation trigger by specifying the arn's of the relevant lambdas in my config. (as per the docs)
My script looks like this:
const aws = require('aws-sdk');
const awsConfig = require('../config/config');
aws.config.update({ region: awsConfig.REGION });
const provider = new aws.CognitoIdentityServiceProvider();
// user provided args
const stage = process.argv[2];
if (!stage) {
process.stdout.write('Please provide stage as argument\n');
process.exit(1);
}
// generate arns for pre and post cognito triggers
const getArn = (lambdaName) => {
return `arn:aws:lambda:${awsConfig.REGION}:${awsConfig.AWS_ACCOUNT_ID}` +
`:function:my-project-name-${stage}-${lambdaName}`;
};
const preSignUp = getArn('preSignUp');
const postConfirmation = getArn('postConfirmation');
const userPoolConfig = {
PoolName: `mypool-${stage}`,
AutoVerifiedAttributes: ['email'],
Schema: [
{
"StringAttributeConstraints": {
"MaxLength": "2048",
"MinLength": "0"
},
"Mutable": true,
"Required": true,
"AttributeDataType": "String",
"Name": "email",
"DeveloperOnlyAttribute": false
}
],
LambdaConfig: {
PostConfirmation: postConfirmation,
PreSignUp: preSignUp
}
};
const callback = (err, resp) => {
if (err) {
process.stdout.write(`${err}\n`);
} else {
process.stdout.write(resp.UserPool.Id);
}
};
provider.createUserPool(userPoolConfig, callback);
When I run this script it successfully creates the user pool, an and when I inspect it in the console the triggers are set correctly.
When I try to register a user on my user pool I get the error:
AccessDeniedException { code: 'UnexpectedLambdaException', ... }
If I go into the console and set the trigger manually it works just fine.
This bug has been reported - but I see no confirmation, nor solution:
https://github.com/aws/aws-cli/issues/2256
Desperately unable to fix or find a workaround.
If you need to add the permission in a serverless.yml file then this is what worked for us. Add it to your Resources section:
UserPoolLambdaInvokePermission:
Type: AWS::Lambda::Permission
Properties:
Action: lambda:invokeFunction
Principal: cognito-idp.amazonaws.com
FunctionName: <function-name>
SourceArn: arn:aws:cognito-idp:<your region>:<your account>:userpool/*
This gives all the user pools the ability to invoke your particular function.
You can get the function name to use by looking in .serverless/serverless-state.json and there against your lambda you'll see the FunctionName property.
If you have set up your user pool and lambda triggers in cloud formation you will need to add the appropirate permissions for the user pool to invoke the lambda function.
You would have to add something like this to your cloud formation template.
"UserPoolPreSignupLambdaInvokePermission" : {
"Type" : "AWS::Lambda::Permission",
"Properties" : {
"Action" : "lambda:invokeFunction",
"Principal" : "cognito-idp.amazonaws.com",
"FunctionName" :{ "Ref" : "AutoVerifyEmailPreSignupLambdaFunction" },
"SourceArn" : {
"Fn::Sub" : "arn:aws:cognito-idp:${AWS::Region}:${AWS::AccountId}:userpool/${UserPool}"
}
}
}
I managed to solve this problem. The issue is that the lambda does not have the correct permissions to interact with cognito.
I found this snippet of information hidden away here
So the in the callback function for the create user pool I attached the correct permissions like this:
const callback = (err, resp) => {
if (err) {
process.stdout.write(`${err}\n`);
} else {
const userPoolId = resp.UserPool.Id;
// the lambdas must have a permission attached that allows them to interact
// directly with cognito
const generateLambdaPersmission = (userPoolName, lambdaName) => {
return {
Action: 'lambda:InvokeFunction',
Principal: 'cognito-idp.amazonaws.com',
SourceArn: `arn:aws:cognito-idp:${awsConfig.REGION}:${awsConfig.AWS_ACCOUNT_ID}:userpool/${userPoolId}`,
FunctionName: getArn(lambdaName),
StatementId: `${stage}1`
};
};
lambda.addPermission(generateLambdaPersmission(userPoolId, 'preSignUp'), (err, resp) => {
if (err) {
process.stdout.write(`error attaching permission to lambda: ${err}`);
}
});
lambda.addPermission(generateLambdaPersmission(userPoolId, 'postConfirmation'), (err, resp) => {
if (err) {
process.stdout.write(`error attaching permission to lambda: ${err}`);
}
});
process.stdout.write(userPoolId);
}
};
See the documentation on adding permissions via the JavaScript SDK here