Creating a Cognito UserPool with lambda's configured as triggers - javascript

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

Related

Auth Privledge FQL query faunaDB create permission denied with udr for collection

I have the following user defined role in security with a predicate function on the create for a collection called formEntryData. Now I can create if I don't have the function which is below.
Under the Create function option
Lambda("values", Equals(Identity(), Select(["data"], Var("values"))))
Now I am creating a request with the following code, which works when the create is just set to all via checking the box, but if I use the function above it fails with permission denied. I must be doing somethign wrong
import { query as q } from "faunadb";
import { serverClient } from "../../utils/fauna-auth";
import { authClient } from "../../utils/fauna-auth";
import { getAuthCookie } from "../../utils/auth-cookies";
export default async (req, res) => {
// const { firstName, lastName, telephone, creditCardNumber } = req.body;
const token = getAuthCookie(req);
console.log(token);
const data = req.body.data;
var element = req.body.data;
element["FormID"] = req.body.id;
try {
await authClient(token).query(
q.Create(q.Collection("FormEntryData"), {
data: element,
})
);
res.status(200).end();
} catch (e) {
res.status(500).json({ error: e.message });
}
};
Any help would be greatly appreciated.
UPDATE: I have also added a index for the collection and given it read permissions in the Role
This was also asked on the Fauna community forums: https://forums.fauna.com/t/roles-membership-auth-token-permissions-denied/1681/4
It looks like two things were needed:
update the create predicate to match the data.user field: Lambda("values", Equals(CurrentIdentity(), Select(["data", "user"], Var("values")))), and
a user field needs to be provided in order to pass the provided predicate.
The answer in the forums used two requests: One to retrieve the calling user document (with CurrentIdentity()), and another to create the FormEntryData document. This can be (should be) done with a single request to limit cost (Every request to Fauna will take at least one Transactional Compute Op), and of course network time for two round trips. Consider the following:
await authClient(token).query(
Let(
{
userRef: q.CurrentIdentity(),
},
q.Create(q.Collection("FormEntryData"), {
data: {
...element,
user: q.Var("userRef")
}
})
)
);

Add custom field to billing agreement using paypal node sdk

Im attempting to add a custom field to a billing agreement so I know what user is starting or stopping their subscription when I recieve he IPN POST call.
const billingAgreementAttributes = {
"name": "TEST Web App",
"description": "TEST WEB APP.",
"start_date": null,
"plan": {
"id": ""
},
"custom": "string"
"payer": {
"payment_method": "paypal",
},
};
api.post('/create', authCheck, (req, res) => {
const body = req.body
const type = body.type // default basic
const user = body.user
let isoDate = new Date();
isoDate.setSeconds(isoDate.getSeconds() + 120);
isoDate.toISOString().slice(0, 19) + 'Z';
let agreement = billingAgreementAttributes
agreement.plan.id = type ? basic : unlimited
agreement.start_date = isoDate
// Use activated billing plan to create agreement
paypal.billingAgreement.create(agreement, function (error, billingAgreement) {
if (error) {
console.error(JSON.stringify(error));
res.json(error)
} else {
console.log("Create Billing Agreement Response");
//console.log(billingAgreement);
for (var index = 0; index < billingAgreement.links.length; index++) {
if (billingAgreement.links[index].rel === 'approval_url') {
var approval_url = billingAgreement.links[index].href;
console.log("For approving subscription via Paypal, first redirect user to");
console.log(approval_url);
res.json({ approval_url })
// See billing_agreements/execute.js to see example for executing agreement
// after you have payment token
}
}
}
});
})
When I add to the billing agreement I get malformed json error.
api.post('/execute', authCheck, (req, res) => {
const token = req.body
console.log(token)
paypal.billingAgreement.execute(token.token, {"custom": "foobar"}, function (error, billingAgreement) {
if (error) {
console.error(JSON.stringify(error));
res.json(error)
} else {
res.json(billingAgreement)
console.log(JSON.stringify(billingAgreement));
console.log('Billing Agreement Created Successfully');
}
});
})
If I try to add it in the data parameter when executing the agreement it is never returned anywhere. So currently if a user were to start or stop a subscription I wont know what user did it.
I am not sure what version of the Subscriptions API and Billing Agreements you are using, but the SDKs do not support the latest version, and a custom parameter is not supported.
You must associate Subscription/billing agreement IDs with a user at creation time, when a user creates one via your site/application. This user-associated Subscription/billing agreement id should be persisted in your own database. Then, any later events on that id can be looked up and matched with the user (although really you want to think of users as having active Subscription/BA ids on file as an object)

Trying to update user attibutes with an AWS lambda function

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);
};

Should I federate cognito user pools along with other social idps, or have social sign in via the userpool itself

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);

"The AWS Access Key Id you provided does not exist in our records" during a federation with Salesforce

I'm trying to establish a federation among Amazon and Salesforce, in this way: if a user correctly authenticates through Salesforce it will see all S3 buckets in the given account.
Quite simple, I followed this blog post and changed something (i.e. I don't use a DyanamoDb table and the callback is for simplicity inside an S3 bucket). The flow that I'm trying to implement is called Enhanced (simplified) flow (details here):
I slightly modified the callback code compared to the article:
function onPageLoad() {
var url = window.location.href;
var match = url.match('id_token=([^&]*)');
var id_token = "";
if (match) {
id_token = match[1];
} else {
console.error("Impossibile recuperare il token");
}
AWS.config.region = "eu-west-1"
const cognitoParams = {
AccountId: "ACC_ID",
IdentityPoolId: "eu-west-1:XXX",
Logins: {
"login.salesforce.com": id_token
}
}
const identity = new AWS.CognitoIdentity()
identity.getId(cognitoParams, function (err, identityData) {
if (err) {
printMessage(err);
return;
}
const identityParams = {
IdentityId: identityData.IdentityId,
Logins: cognitoParams.Logins
}
identity.getCredentialsForIdentity(identityParams, function (err, data) {
if (err) {
printMessage(err);
} else {
var c = {
region: 'eu-west-1',
accessKeyId: data.Credentials.AccessKeyId,
secretAccessKey: data.Credentials.SecretKey
};
var s3 = new AWS.S3(c);
// HERE IS THE ERRORE - data is empty and response contains the error
s3.listBuckets((response, data) => {
data.Buckets.forEach(function (value) { appendMessage(value.Name) })
});
}
});
});
// IRRELEVANT CODE
}
I can get the token from Salesforce, I can get the access and secret keys but when I try to list the buckets I get a laconic:
The AWS Access Key Id you provided does not exist in our records.
I found this error reasonable since I have no user at all and the keys are created on-the-fly. Where can I hit my head? The SDK is 2.103.0.
Could be due to eventual consistency of IAM, can you try to include a delay before calling the listbucket api or make the request to us-east-1 endpoint?
http://docs.aws.amazon.com/IAM/latest/UserGuide/troubleshoot_general.html#troubleshoot_general_access-denied-service2.
GetCredentialsForIdentity returns temporary credentials. So you should include AccessKeyId, SecretKey and SessionToken to make the request.
http://docs.aws.amazon.com/cognitoidentity/latest/APIReference/API_GetCredentialsForIdentity.html
Hope this helps.

Categories