Get Facebook user details when user interacts with chatbot - facebook-graph-api

I'm new to Dialogflow and I wanted to integrate it to Facebook Messenger using webhooks. Problem is that I don't know how GRAPH API works. I already created my chatbot and tested it on the console. It works like this:
User: book appointment
Bot: Ask for credentials (username, password, etc.)
After validation it then saves it to my database. The problem with this is that the chatbot will ask the user password when asking an update for appointment status.
I wanted to integrate to Facebook Messenger because the chatbot won't be constantly asking for the user password as it will use the FB:ID to verify the user account.
Any idea how to translate it to code? I'm using nodejs to write my code?

Post an answer as community wiki, as this solution has solved the issue. The solution based on Graph Api posted on this answer:
You need to make a call to Facebook Graph API in order to get user's profile.
Facebook offers some SDKs for this, but their official JavaScript SDK is more intended to be on a web client, not on a server. They mention some 3rd party Node.js libraries on that link. I'm particularly using fbgraph (at the time of writing, it's the only one that seems to be "kind of" maintained).
So, you need a Page Token to make the calls. While developing, you can get one from here:
https://developers.facebook.com/apps/<your app id>/messenger/settings/
Here's some example code:
const { promisify } = require('util');
let graph = require('fbgraph'); // facebook graph library
const fbGraph = {
get: promisify(graph.get)
}
graph.setAccessToken(FACEBOOK_PAGE_TOKEN); // <--- your facebook page token
graph.setVersion("3.2");
// gets profile from facebook
// user must have initiated contact for sender id to be available
// returns: facebook profile object, if any
async function getFacebookProfile(agent) {
let ctx = agent.context.get('generic');
let fbSenderID = ctx ? ctx.parameters.facebook_sender_id : undefined;
let payload;
console.log('FACEBOOK SENDER ID: ' + fbSenderID);
if ( fbSenderID ) {
try { payload = await fbGraph.get(fbSenderID) }
catch (err) { console.warn( err ) }
}
return payload;
}
Notice you don't always have access to the sender id, and in case you do, you don't always have access to the profile. For some fields like email, you need to request special permissions. Regular fields like name and profile picture are usually available if the user is the one who initiates the conversation. More info here.
Edit
Promise instead of async:
function getFacebookProfile(agent) {
return new Promise( (resolve, reject) => {
let ctx = agent.context.get('generic');
let fbSenderID = ctx ? ctx.parameters.facebook_sender_id : undefined;
console.log('FACEBOOK SENDER ID: ' + fbSenderID);
fbGraph.get( fbSenderID )
.then( payload => {
console.log('all fine: ' + payload);
resolve( payload );
})
.catch( err => {
console.warn( err );
reject( err );
});
});
}

Related

How to link my mobile hub with my existing cognito user pool? (Part 2)

I am sorry to do this, but I don't have enough reputation to comment on this issue: How to link my mobile hub with my existing cognito user pool?, and the answer provided didn't quite solve my problem.
To explain a little better, I am using the React Native build-out for AWS Mobile Hub. I have "successfully" authenticated, but Amplify keeps using the pre-configured (automatically integrated) User Pool. This requires the user to enter a phone number as part of the sign up process, but we don't have that field for sign up.
I have linked my Cognito User Pool to the pre-configured (automatically integrated) Identity Pool under "Authentication Providers" as #andrew-c suggested in the issue above. I edited the 'aws_user_pools_id' property to point to my custom User Pool, as suggested in the issue above.
When I console.log() the User Pool property, that I passed into my Amplify configuration, it gives me my custom User Pool.
When I console.log() the response from the Authentication success, I also see my custom User Pool. But the user keeps getting saved into, and authenticated against the model (namely, phone_number is required) of the pre-configured (automatically integrated) User Pool. I.e. that's where my users show up after sign-up.
Does anyone know what I'm missing?
Here's what I'm seeing in the console:
That's what I get when running this code:
const attributes = {
name: this.state.name,
birthdate: this.state.birthdate,
phone_number: '+011234567890',
}
try {
console.log(Auth.signUp);
this.setState({ loading: true });
const response = await Auth.signUp({
username: this.state.email,
password: this.state.password,
attributes,
validationData: [],
});
console.log(`SignUp::onSignUp(): Response#1 = ${JSON.stringify(response, null, 2)}`);
if (response.userConfirmed === false) {
this.setState({ authData: response, modalShowing: true, loading: false });
} else {
this.onAuthStateChange('default', { username: response.username });
}
} catch (err) {
console.log(`SignUp::onSignUp(): Error ${JSON.stringify(err, null, 2)}`);
this.setState({ error: err.message, loading: false });
}
And the relevant imports:
import Amplify, { Auth } from 'aws-amplify';
import aws_exports from '../../aws-exports';
Amplify.configure(aws_exports);
So suffice it to say I would really like to not require a phone number on sign up, and I figured this would be the easiest way, but if anyone knows what I'm missing or has a better path for me to go down, please let me know. Thanks folks!

Expo, React Native, Share/upload picture to Facebook (story on my own page)

I am really lost here, I have an Expo React-Native application. the user can login with Facebook credentials.
loginWithFacebook = async () => {
const { type, token } = await Expo.Facebook.logInWithReadPermissionsAsync(FACBOOK_APP_ID, {
permissions: ['public_profile', 'email'],
});
if (type === 'success') {
this.callGraph(token);
} else {...});
}
};
and
callGraph = async (token) => {
const response = await fetch(`https://graph.facebook.com/me?access_token=${token}&fields=id,name,email,picture`);
const responceJSON = JSON.stringify(await response.json());
try {
await AsyncStorage.multiSet([['FBResponse', responceJSON], ['FBToken', token]]);
} catch (error) {...}
};
now I have the Token and info of the loged in user. now I get a picture from cameraRoll
pickImage = async () => {
const result = await Expo.ImagePicker.launchImageLibraryAsync({...});
};
which gives the URI of the image at the device, also I am using RNFetchBlob to again have the image as base64.
but I am really confused that how should I share/upload this picture to Facebook. Should I eject from Expo install FBSDK and do it from Android folder? or I can do it with the Graph API? or I can Link it to Expo?
I could not find a single example, just simple react-native-share samples are what I see everywhere.
Facebook integration for Expo only accepts read permissions and one cannot login with publish_actions it seems the community is still working on it. and for using fbsdk you should either eject from Expo or ask for permissions via the Expo WebBrowser integration.

Cognito auth flow fails with "Already found an entry for username Facebook_10155611263153532"

The goal is to implement a social provider auth flow as described in User Pools App Integration and Federation.
One important thing that I want to satisfy, is to merge user pool accounts that have the same email address.
I am accomplishing that by calling adminLinkProviderForUser within the PreSignUp_ExternalProvider cognito lambda trigger.
So with this, everything works. The new social provided user is being registered and linked with the already existing Cognito (user+pass) user.
However, the authentication flow, from user's perspective doesn't complete. It fails at the last step where the callback uri (defined in cognito user pool) is being called:
error: invalid_request
error_description: Already found an entry for username Facebook_10155611263152353
But then, if the user retries the social auth flow, everything works, and would get session tokens that represent the original Cognito User Pool user (the one that already had that email).
Note that I'm testing the auth flow on an empty User Pool, zero user accounts.
For all the poor souls fighting with this issue still in 2020 the same way I did:
I have eventually fixed the issue by catching the "Already found an entry for username" in my client application and repeating the entire auth flow once more.
Luckily the error only gets fired on the initial external provider signup but not in the subsequent signins of the same user (cause it happens during signup trigger, duh).
I'm taking a wild guess, but here is what I think is happening:
In my case, the facebook provider was getting succesfully linked with
the pre-existing cognito email/password user. new Facebook userpool
entry linking to the email/password user was succesfully created.
Still, it seems
like cognito tried to register the fully isolated Facebook_id user
during the internal signup process (even though a link user entry with the same username was already created in the previous step). Since the "link user" with the
username Facebook_id was already existing, cognito threw an
"Already found an entry for username Facebook_id error" internal error.
This error has been repeatedly voiced over to the AWS developers since 2017 and there are even some responses of them working on it, but in 2020, it's still not fixed.
Yes, this is how it is currently setup. If you try to link users using PreSignUp trigger, the first time won't work. A better way to handle this(I think) would be to provide an option in your UI to link external accounts on sign-in. In the pre-signup trigger, search for a user with the same unique attribute (say email) and see if the sign up is from external provider. Then show a message such as email already exists. Login in & use this menu/option to link. Haven't tested this though.
To elaborate on #agent420's answer, this is what I am currently using (Typescript example).
When a social identity attempts to sign up and the email address already exists I catch this using the PreSignUp trigger and then return an error message to the user. Inside the app, on the user's profile page, there is an option to link an identity provider which calls the adminLinkProviderForUser API.
import {
Context,
CognitoUserPoolTriggerEvent,
CognitoUserPoolTriggerHandler,
} from 'aws-lambda';
import * as aws from 'aws-sdk';
import { noTryAsync } from 'no-try';
export const handle: CognitoUserPoolTriggerHandler = async (
event: CognitoUserPoolTriggerEvent,
context: Context,
callback: (err, event: CognitoUserPoolTriggerEvent) => void,
): Promise<any> => {
context.callbackWaitsForEmptyEventLoop = false;
const { email } = event.request.userAttributes;
// pre sign up with external provider
if (event.triggerSource === 'PreSignUp_ExternalProvider') {
// check if a user with the email address already exists
const sp = new aws.CognitoIdentityServiceProvider();
const { error } = await noTryAsync(() =>
sp
.adminGetUser({
UserPoolId: 'your-user-pool-id',
Username: email,
})
.promise(),
);
if (error && !(error instanceof aws.AWSError)) {
throw error;
} else if (error instanceof aws.AWSError && error.code !== 'UserNotFoundException') {
throw error;
}
}
callback(null, event);
};
I finally got this thing working in a non-weird way where users have to authorize twice or other things.
Process explained:
User tries to authenticate using an identity provider, for the first time => PreSignUp lambda kicks in and check if user exists via email
1a. If the user exists, it will throw an error, eg. CONFIRM_IDENTITY_LINK_token that I'm capturing on the client.
token is a base64 string with the username and identity id ("username:facebook_123456")
1b. If the username does not exist, I create a new user with a temporary password and throw an error FORCE_CHANGE_PASSWORD_token. Same token but I add the temporary password to this time.
In the client I have one callback route '/authorize' => this is the one you set up as a callback URL in Cognito, and 2 extra routes: '/confirm-password' and '/configure-password'.
In the /authorize route I'm capturing the errors and getting the attached tokens and redirect to the extra routes: 1a => /configure-password?token=token and 1b => /confirm-password?token=token
For "/confirm-password" I ask the user to confirm its current password in order to authorize linking with the provider, then use the token to log him in with the identity id as clientMetadata, eg "{"LINK_PROVIDER": "facebbok_12345678"}"
On login, I have a PostAuthentication lambda which checks for the "LINK_PROVIDER" in the clientMetadata, and links it to the user.
For "/configure-password" I parse the token and do a "shallow" login with the credentials from the token and identity id as client metadata (same as above) then prompt the user to configure a new password for his account.
I know it might seem a little bit restrictive but I find it better than to authorize twice.
Also, this does not create extra users for identities in the user pool.
Code examples:
PreSignUp lambda
export async function handler(event: PreSignUpTriggerEvent) {
try {
const { userPoolId, triggerSource, request, userName } = event
if (triggerSource === 'PreSignUp_ExternalProvider') {
// Check if user exists in cognito
let currentUser = await getUserByEmail(userPoolId, request.userAttributes.email)
if (currentUser) {
// User exists, thow error with identity id
const identity = Buffer.from(`${currentUser}:${userName}`).toString('base64')
throw new Error(`CONFIRM_USER_IDENTITY_${identity}`)
}
// Create new Cognito user with temp password
const tempPassword = generatePassword()
currentUser = await createNewUser(userPoolId, request.userAttributes, tempPassword)
// Throw error with token
const state = Buffer.from(`${currentUser}:${tempPassword}:${userName}`).toString('base64')
throw new Error(`FORCE_CHANGE_PASSWORD_${state}`)
}
return event
} catch (error) {
throw new Error(error)
}
}
PostAuthentication lambda
export async function handler(event: PostAuthenticationTriggerEvent) {
try {
const { userPoolId, request, userName } = event
if (request.clientMetadata?.LINK_IDENTITY) {
const identity = request.clientMetadata['LINK_IDENTITY']
// Link identity to user
await linkIdentityProvider(userPoolId, userName, identity)
}
return event
} catch (error) {
console.error(error)
throw new Error('Internal server error')
}
}
We faced the same issue and tried various hacks to get around it. As we started to use SignInWithApple, we couldn't handle it with the 'double turnaround' because Apple always wants the user to enter their email and password, not like Google, where the second time, everything works automatically. So the solution we ended up building was to store the Cognito/IdP ID (Google_1234, SignInWithApple_XXXX.XXX.XXX) in our database but still create a native Cognito user that isn't linked via Cognito.
The native user is created to make unlinking easier because first, we get rid of the data (IdP user-id) we store in our database and then the Cognito IdP user. The user can then proceed using the Native Cognito user. Then we have a middleware component in place that allows us to have JWT in the external IdP or Cognito native format and translates so we can use both versions. As long as the user uses an IdP/SSO, we reset the Native users' password to a very long random value and prevent resetting it, so they must use the IdP.
So whatever you are trying to do, prevent using the admin-link-provider-for-user command!
The same code in JavaScript getUser has been called instead of listUsers. It is also assumed that all users have their email id as their username.
const aws = require('aws-sdk');
exports.handler = async (event, context, callback) => {
console.log("event" + JSON.stringify(event));
const cognitoidentityserviceprovider = new aws.CognitoIdentityServiceProvider({apiVersion: '2016-04-18'});
const emailId = event.request.userAttributes.email
const userName = event.userName
const userPoolId = event.userPoolId
var params = {
UserPoolId: userPoolId,
Username: userName
};
var createUserParams = {
UserPoolId: userPoolId,
Username: emailId,
UserAttributes: [
{
Name: "email",
Value: emailId
},
],
TemporaryPassword: "xxxxxxxxx"
};
var googleUserNameSplitArr = userName.split("_");
var adminLinkUserParams = {
DestinationUser: {
ProviderAttributeName: 'UserName',
ProviderAttributeValue: emailId,
ProviderName: "Cognito"
},
SourceUser: {
ProviderAttributeName: "Cognito_Subject",
ProviderAttributeValue: googleUserNameSplitArr[1],
ProviderName: 'Google'
},
UserPoolId: userPoolId
};
var addUserToGroupParams = {
GroupName: "Student",
UserPoolId: userPoolId,
Username: emailId
};
if (userName.startsWith("Google_")) {
await cognitoidentityserviceprovider.adminGetUser(params, function (err, data) {
if (err) {
console.log("No user present")
console.log(err, err.stack);
cognitoidentityserviceprovider.adminCreateUser(createUserParams, function (err, data) {
if (err) console.log(err, err.stack);
else {
console.log("User Created ")
cognitoidentityserviceprovider.adminAddUserToGroup(addUserToGroupParams, function (err, data) {
if (err) console.log(err, err.stack);
else {
console.log("added user to group");
console.log(data);
}
});
cognitoidentityserviceprovider.adminLinkProviderForUser(adminLinkUserParams, function (err, data) {
if (err) console.log(err, err.stack);
else {
console.log("user linked");
console.log(data);
}
});
console.log(data);
}
});
} else {
console.log("user already present")
cognitoidentityserviceprovider.adminLinkProviderForUser(adminLinkUserParams, function (err, data) {
if (err) console.log(err, err.stack); // an error occurred
else {
console.log("userlinked since user already existed");
console.log(data);
}
});
console.log(data);
}
});
}
console.log("after the function custom");
callback(null, event);
};
This is a well know error. I handle it by retrying the request after this error and it will work. The error is because there is not way in the SDK to let it know to the pool that you already link the Federation Credentials to an user and it try to create a new user with those credentials
I wanted to have the feature of having a user seamlessly being able to login with one social provider (ex: Facebook) and then another one (Google).
I struggled with the retry process, especially with Google Login. At the signup process, if a user have several accounts, he will need to process twice the account selection.
What I ended up doing is just using Cognito for the client side code and token generation and have a lambda in the pre signup process mapping userIds with their email in a custom DB (Postgres or DynamoDB).
Then when a user query my API, based on their userId (whether it's a FacebookId or a cognito email userId, I am querying the DB to find the linked email and I am able to authenticate any users and their data like this.
Did this bug all of a sudden stop happening on 2/21/23? We didn't change anything but now this is no longer happening to users on their first time signing up. We also noticed that the UI for how Cognito is showing linked users is different - there is just 1 cognito account you're able to see in Cognito instead of multiple. You can still see the federated linked accounts in the identities property though

Is an AWS user pool sync trigger possible?

Question
I have a lambda function triggered when someone registers via federated identities that creates an entry in a dynamodb table.
I want the same function (or similar) to occur when a user registers (I was thinking post confirm) via an associated user pool.
Background (what I've attempted)
I've linked the federated identity to the user pool but the lambda linked to the Cognito trigger does not get called for user pools. I thought it may not support the same process (is this the case?) and tried adding a customised workflow trigger to the user pool for post confirm. I just get an error back (bad request 400) stating '{"__type":"NotAuthorizedException","message":"User cannot confirm."}' although the user is showing as confirmed in Cognito.
I've looked at the documentation but I don't see many clear examples. The best I found was one emailing on post confirm which I modified to contain a basic dynamo call as follows:
var doc = require('dynamodb-doc');
exports.handler = function(event, context) {
console.log(event);
if (event.request.userAttributes.email) {
var db = new doc.DynamoDB();
var tableName = 'Users'
var user = {
'id' : event.identityId,
'name' : event.datasetRecords.name.newValue,
'email' : event.datasetRecords.email.newValue,
};
var params = {
'TableName' : tableName,
'Item' : user
};
console.log('Inserting user', params);
db.putItem(params, function(err, data) {
console.log(err, data);
if (err) {
console.log('User insert failure', err);
context.done(err);
} else {
console.log('User insert success', data);
context.done(null, event);
}
});
} else {
// Nothing to do, the user's email ID is unknown
context.done(null, event);
}
};
I've looked at similar questions and nearest I found was this
previous question
although it does not include a working code snippet. I've tried a few variations but no luck!
As stated there I have also seen callbacks used in other examples so it would be good to clear up what the preferred and working code should look like!
I would also like to know if it should return data within the context.done in a particular format as I saw some set responses like the following:
"response": {
}
Many thanks!
Calling ConfirmSignUp on already confirmed user throws the error "User cannot confirm". As a result of that post confirmation lambda function is not called. Though this error could be more descriptive.
Have you looked at this example from the docs already?
http://docs.aws.amazon.com/cognito/latest/developerguide/cognito-user-identity-pools-working-with-aws-lambda-triggers.html#aws-lambda-triggers-post-confirmation-example

Sails REST API with simple AUTH

I'm pretty new to sails, but after read the doc and followed some examples at the Internet, I decided to give it a shot ;)
I have made an APP that depend on a REST webservice that I want to build in Sails Framework - but after a lots of research I haven't found the right solutions in sails yet.
I think I want to pass a (username, password) or a api_key in each webservice call made from the app?
All the examples that i found was only with a session login method - not with an API key in each call.
I used this tutorial - http://jethrokuan.github.io/2013/12/19/Using-Passport-With-Sails-JS.html
But only logins at post to login page - I want it to login in every call and still want to use the build in REST API blueprints.
The problem in my solution is that a call to like this - will not give me all the users as expected because of the default REST method - I want it to auth the user and give me the result ..
http://example.com:1337/user/?username=test&password=xxx
What is the "best practises" for building a APP with a REST webservice backend? - "with sails"
Some of my auth code:
// policies/authentication.js
if(req.param('username') && req.param('password')) {
UserAuth.auth(req, res, function(err, user) {
if (err) return res.forbidden('You are not permitted to perform this action.');
if(user) {
return next();
}
});
}else{
return res.forbidden('You are not permitted to perform this action.');
}
// services/UserAuth.js
module.exports = {
auth : function(req, res, cb) {
var bcrypt = require('bcrypt');
var passport = require("passport");
passport.authenticate('local', function(err, user, info){
if (err) return cb({ error: 'auth error!', status: 400 });
if(user) {
cb(null, user);
}
})(req, res);
}
}
// config/policies.js
module.exports.policies = {
'*': "authentication"
};
First off, it's bad practice to continuously expose usernames and passwords in the wild like this. At the very least, you should consider issuing access_tokens that expire after some time, and need to be re-issued via a login system.
Second, if you want to authenticate on every request (instead of using sessions), it's better to do so using a request header, rather than putting the credentials in the query string. This is especially true when using Sails blueprints; otherwise you'll have to do extra work to keep the blueprints from using your credentials as search criteria.
When using a header, per-request authorization becomes simple with Sails. Set up a policy in api/policies called (for example) auth.js:
module.exports = function (req, res, next) {
// Find an access header
var accessToken = req.header('my-auth-header');
// No header, no access
if (!accessToken) {return res.forbidden();}
// Find the user with that token
User.findOne({accessToken: accessToken})
.exec(function(err, user) {
// Handle error
if (err) {return next(err);}
// Handle bad access token
if (!user) {return res.forbidden();}
// Handle success
return next();
});
}
Then you can set any controller actions that need authentication using the config/policies.js file:
module.exports = {
SomeController: {
'*': 'auth'
},
...etc...
}