I'm trying to implement the Facebook OAuth in my express/NodeJS app using authorization code flow. I'm using react-facebook-login node module to fetch the authorization code. In my react app, I could get the authorization code successfully. But in server side, I can't request the access token from the Facebook API as I'm getting an error message "redirect_uri is not identical to the one you used in the OAuth dialog request"
Code in my react app,
facebookLogin = async (signedRequest) => {
return fetch('/api/auth/facebook', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ signedRequest }),
}).then((res) => {
if (res.ok) {
return res.json();
} else {
return Promise.reject(res);
}
});
};
responseFacebook = async (response) => {
try {
if (response['signedRequest']) {
const userProfile = await this.facebookLogin(response['signedRequest']);
console.log(userProfile);
} else {
throw new Error(response.error);
}
} catch (err) {
console.log(err);
}
};
render() {
<FacebookLogin
appId={process.env.FACEBOOK_CLIENT_ID}
fields="name,email"
responseType="code"
redirectUri="http://localhost:3000/"
callback={this.responseFacebook}
/>
In my app.js
const facebookOAuth = require('./config/facebookOAuth');
// facebook oauth route
app.post("/api/auth/facebook", async (req, res) => {
try {
const signedRequest = req.body.signedRequest;
const profile = await facebookOAuth.getProfile(signedRequest);
console.log(profile);
res.send({ profile });
} catch (err) {
console.log(err);
res.status(401).send();
}
});
facebookOAuth.js look like this
const fetch = require('node-fetch');
const getData = async (userId, accessToken) => {
const userData = await fetch(`https://graph.facebook.com/${userId}?fields=name,email&access_token=${accessToken}`, {
method: 'GET'
}).then((res) => {
return res.json();
}).then((userData) => {
return userData;
});
return userData;
};
exports.getProfile = async (signedRequest) => {
const decodedSignedRequest = JSON.parse(Buffer.from((signedRequest.split(".")[1]), 'base64').toString());
const profile = await fetch(`https://graph.facebook.com/oauth/access_token?client_id=${process.env.FACEBOOK_CLIENT_ID}&redirect_uri=${encodeURIComponent('http://localhost:3000/')}&client_secret=${process.env.FACEBOOK_CLIENT_SECRET}&code=${decodedSignedRequest.code}`, {
method: 'GET'
}).then((res) => {
return res.json();
}).then((token) => {
console.log(token);
const userData = getData(decodedSignedRequest.user_id, token.access_token);
return userData;
}).catch((err) => {
console.log(err);
return err;
});
return profile;
}
What I'm getting is this error
"error": {
message: 'Error validating verification code. Please make sure your redirect_uri is identical to the one you used in the OAuth dialog request',
type: 'OAuthException',
code: 100,
error_subcode: 36008,
fbtrace_id: 'A-YAgSqKbzPR94XL8QjIyHn'
}
I think the problem lies in my redirect_uri. Apparently, the redirect uri I obtained from the Facebook auth dialog is different from the one that I'm passing to the facebook API in my server side (http://localhost:3000/).
I believe there's something to do with the origin parameter of the redirect_uri. Initial auth dialog request uri indicates that it's origin parameter value is something like "origin=localhost:3000/f370b6cb4b5a9c". I don't know why react-facebook-login add some sort of trailing value at the end of origin param.
https://web.facebook.com/v2.3/dialog/oauth?app_id=249141440286033&auth_type=&cbt=1620173773354&channel_url=https://staticxx.facebook.com/x/connect/xd_arbiter/?version=46#cb=f39300d6265e5c4&domain=localhost&origin=http%3A%2F%2Flocalhost%3A3000%2Ff370b6cb4b5a9c&relation=opener&client_id=249141440286033&display=popup&domain=localhost&e2e={}&fallback_redirect_uri=http://localhost:3000/&locale=en_US&logger_id=f1b3fba38c5e31c&origin=1&redirect_uri=https://staticxx.facebook.com/x/connect/xd_arbiter/?version=46#cb=f17641be4cce4d4&domain=localhost&origin=http%3A%2F%2Flocalhost%3A3000%2Ff370b6cb4b5a9c&relation=opener&frame=f3960892790a6d4&response_type=token,signed_request,graph_domain&return_scopes=false&scope=public_profile,email&sdk=joey&version=v2.3
I tried finding everywhere about this but no luck. Anyone has clue about this, much appreciated.
Are you using middleware to parse the body? if you aren't code could be undefined here.
const facebookOAuth = require('./config/facebookOAuth');
// facebook oauth route
app.post("/api/auth/facebook", async (req, res) => {
try {
const code = req.body.code;
const profile = await facebookOAuth.getProfile(code);
console.log(profile);
res.send({ profile });
} catch (err) {
console.log(err);
res.status(401).send();
}
});
I'm having a problem using Axios with my backend. It's probably a very simple fix as I'm new to this.
Postman: The correct response is received for both valid and invalid credentials.
Axios: The correct response is received for valid crendentials, but the axios method's catch block is run when invalid credentials are entered.
authController.js:
exports.login = (req, res, next) => {
const email = req.body.email;
const pass = req.body.password;
let loadedUser;
User.findOne({ where: { email: email } })
.then(user => {
if(!user) {
const error = new Error('Incorrect username or password');
error.statusCode = 401;
throw error;
} else {
loadedUser = user;
return bcrypt.compare(pass, user.password);
}
})
.then(isEqual => {
if(!isEqual) {
const error = new Error('Incorrect username or password');
error.statusCode = 401;
throw error;
} else {
const token = jwt.sign(
{
email: loadedUser.email,
userId: loadedUser.id
},
process.env.JWT_SECRET,
{ expiresIn: '1hr' }
);
res.status(200).json({ token: token, userId: loadedUser.id });
}
})
.catch(err => {
if (!err.statusCode)
err.statusCode = 500;
next(err);
});
};
The error handler in app.js. It seems to log the error correctly when incorrect credentials are entered, even with axios:
app.use((error, req, res, next) => {
const status = error.statusCode || 500;
const message = error.message;
const data = error.data || 'No Data';
console.log(status, message, data);
res.status(status).json({message: message, data: data});
});
But then the axios catch block runs, so instead of receiving the json message, I get the following error
login(email, password) {
const headers = {
'Content-Type': 'application/json'
};
const data = JSON.stringify({
email: email,
password: password
});
axios.post('http://127.0.0.1:8080/auth/login', data, { headers })
.then(res => console.log(res))
.catch(err => console.log(err));
}
The error in the console for invalid credentials:
Clicking the link highlighted opens a new page stating: "Cannot GET /auth/login", but I'm obviously making a post request, & I've added post to the form (just in case)
Any ideas what I could be missing?
Thanks
Actually your code works fine but Axios will reject the promise of the call if you have the status 401. If you have a status between 200 to 300 it will resolve the promise.
There two ways to deal with this.
Check status in the catch block.
axios.post('http://127.0.0.1:8080/auth/login', data, {
headers
})
.then(res => console.log(res))
.catch(err => {
if (err.response.status === 401) {
//Auth failed
//Call reentry function
return;
}
return console.log(err)
});
or change the validateStatus option;
axios.post('http://127.0.0.1:8080/auth/login', data, {
headers,
validateStatus: function (status) {
return status >= 200 && status < 300 || (status === 401);
},
})
.then(res => console.log(res))
.catch(err => return console.log(err));
I am running a react app with nodejs acting as an api to connect to my database.
For my log in I am sending data to the server, and it is returning a pass or fail.
However I am not sure how to extract this json object.
I have looked at the request and response, and as I have manipulated the json object the response content-length has been changing so I believe it must be there somewhere.
SERVER CODE:
app.post('/api/checkLogin', async (req,res) => {
console.log(req.body);
const {username, password} = req.body;
try{
let state = await DB.checkPassword(username, password);
console.log(state);
if(!state){
res.status(401).json({
error: 'Incorrect username or password',
yay: 'idk work?'
});
}
else if(state){
res.status(200).json({
message: 'we in boys'
});
} else {
res.status(6969).json({
err: 'idk mane'
});
}
} catch(e) {
console.log(e);
}
})
CLIENT CODE:
onSubmit = (event) => {
event.preventDefault();
fetch('/api/checkLogin', {
method:'POST',
body: JSON.stringify({username: this.state.username, password: md5(this.state.password)}),
headers: {
'Content-Type':'application/json'
}
}).then(res => {
if(res.status ===200) {
this.props.loggedIn();
} else if(res.status ===401){
console.log(res.status);
alert('wrong username or password');
}else{
const error = new Error(res.error);
throw error;
}
}).catch(err => {
console.log(err);
alert(err);
});
}
What I was sort of expecting as a way to extract the data would be.
On the server:
res.status(200).json({ message : 'mssg'});
On the client:
console.log(res.status.message) // 'mssg'
Thanks Jin and this post I found for the help Fetch API get raw value from Response
I have found that both
res.status(xxx).json({ msg: 'mssg'}) and res.status(xxx).send({msg: 'mssg'}) work.
The json, or sent message can then be interpreted on the client side with a nested promise. This is done with...
fetch('xxx',headers n stuff).then(res => {
res.json().then((data) => {console.log(data.message)});
//'mssg'
res.text().then((data) => { let data1 = JSON.parse(data); console.log(data1.message);});
//'mssg'
});
According to my experience, using res.status(200).send({message: 'mssg'}) is better.
And you can get data after calling api by using res.data.
Then you can get result as below:
{
message: 'mssg'
}
Here is something that may help.
onSubmit = (event) => {
event.preventDefault();
const userData = {
username: this.state.username, // I like to store in object before passing in
password: md5(this.state.password)
}
fetch('/api/checkLogin', {
method:'POST',
body: JSON.stringify(userData), //stringify object
headers: {
'Content-Type':'application/json'
}
}).then(res => res.json()) // convert response
.then(responseData => {
let status = responseData.whatObjectWasPassedFromBackEnd;
status === 200 ? do something on pass: do something on fail
})
.catch(err => {
console.log(err);
alert(err);
});
}
I can't seem to get the correct response headers when my code enters bcrypt.compare. I thought it was a cors issue at first but I still get the correct response if I entered the wrong and "user does not exist" is displayed.
Here's my api server side code in express
router.post("/api/signIn", (req, res) => {
const { user, password } = req.body;
const queryString = "SELECT * FROM users WHERE user_id = ?";
db.query(queryString, [user])
.then(result => {
if (result.length > 0) {
const hash = result[0].password;
//here bcrypt.compare works server side or with cURL but won't set the response headers in the browser
bcrypt
.compare(password, hash)
.then(same => {
if (same === true) {
console.log("correct password");
res.status(200).json({
code: 200,
message: "correct password"
});
} else {
res.status(401).json({
code: 401,
message: "incorrect password"
});
}
})
.catch(err => console.log(err));
} else {
//this one works though and i can get the response in the browser so it can't be a cors issue
console.log("user does not exist");
res.status(200).json({
code: 401,
message: "User does not exist"
});
}
})
.catch(err => {
console.log("error" + err.message);
});
});
and this is the test function i use in react
const signIn = () => {
fetch("http://localhost:5000/api/signIn", {
method: "POST",
body: JSON.stringify({
user: userName,
password: password
}),
headers: {
"Content-Type": "application/json"
},
})
.then(res => res.json())
.then(response => alert(response.code + response.message))
.catch(err => alert(err));
};
so if i entered the wrong username that is not in the database, the alert function would show (code401User does not exist) but if i entered the correct user bcrypt.compare() doesn't seem to set the response for both correct and incorrect passwords and i would get (TypeError: failed to fetch). testing the api in cURL works though.
Got it, I forgot to put event.preventDefault() on the fetch function.
My external routes is setup so that a POST form sends data to my server and my server file validates the input. It then takes this input and passes it to another file which will then perform authentication. I have a few questions regarding this, which are:
1) Do I need to pass the data asynchronously?
2) Is the way I am currently passing synchronous? Should it be changed
3) If I want to pass my variables into this file and it uses an async waterfall, (I believe I can't pass variables into the first function) should I create a main function which has two sub functions (one for storing the variables and one for executing the async waterfall)?
I am new to Node.js so have only just started learning the concept of asynchronous functionality. Any tips, help or questions, please let me know. If i have left something out please ask
My Server function is as such:
//Route for POST requests to Login
//First Sanitise input!
app.route('/login').post([
//ACCOUNTTYPE CHECKS -------------------------------------------------------------------------------
check('accountType').exists()
.trim()
.isAlpha()
.isLength({
min: 8,
max: 10
})
.matches(/(Production|Internal)/).withMessage('Must be either \'Production\' or \'Internal\'\!')
.escape(),
//CUSTOMERID CHECKS --------------------------------------------------------------------------------
check('customerID').exists()
.trim()
.isNumeric()
.isInt()
.isLength({
max: 40
})
.escape(),
//CLIENTID CHECKS ---------------------
check('clientID').exists()
.trim()
.isAlphanumeric()
.isLength({
max: 40
})
.escape(),
//CLIENTSECRET CHECKS ---------------------
check('clientSecret').exists()
.trim()
.isAlphanumeric()
.isLength({
min: 0,
max: 45
})
.escape(),
//USERNAME CHECKS ---------------------
check('username').exists()
.trim()
.isEmail()
.isLength({
min: 0,
max: 100
})
.escape(),
//PASSWORD CHECKS ---------------------
check('password').exists()
.trim()
.isLength({
min: 0,
max: 100
})
.escape()
], urlencodedParser, (req, res) => {
let now = new Date().toString(),
log = `${now}: ${req.method} ${req.url}`,
sess = req.session;
console.log(req.body);
fs.appendFile('server-requests.log', log + '\n', (err) => {
if (err) {
console.log('Unable to append to server-requests.log!');
}
});
// Finds the validation errors in this request and wraps them in an object with handy functions
const errors = validationResult(req);
if (!errors.isEmpty()) {
//return a bad status and array of errors
//if the validation fails
// return res.status(422).json({
// errors: errors.array()
// });
//OR:
return res.render('login.hbs', {
currentYear: new Date().getFullYear(),
//This does not work realtime - FIX TODO
currentTime: moment().format('MMMM Do YYYY - h:mm:ss a'),
data: {},
errors: errors.mapped()
});
// If there aren't any errors, proceed to save variable data and use 2.0
} else {
//Handle with event emiitters and make asynchronous - TODO
const data = matchedData(req);
console.log('Data stored in session:')
console.log('Sanitised:', data);
//Store Form Data in session variables
sess.accountType = data.accountType,
sess.customerID = data.customerID,
sess.clientID = data.clientID,
sess.clientSecret = data.clientSecret,
sess.username = data.username,
sess.password = data.password;
console.log('Sanitised POST Form! Now initialising request.');
req.flash('success', 'Successful POST request, proceeding to authorisation...)');
//Make asynchronous?
//use each function and get variables
auth.saveVariables(sess.accountType, sess.customerID, sess.clientID, sess.clientSecret, sess.username, sess.password);
auth.asyncWaterfall(callback)
res.redirect('/index');
//Or alternatively should it redirect with a query string?
// const url = require('url');
// app.get('/index', function (req, res) {
// res.redirect(url.format({
// pathname: "/",
// query: {
// "acccountType": 1,
// "customerID": 2,
// "clientID": 3,
// "clientSecret": 4,
// "username": 5,
// "password": 6
// }
// }));
// });
}
})
Here is the start of my auth controller code:
const saveVariables = (accountType, customerID, clientID, clientSecret, username, password, callback) => {
//Test parameters
if (accountType.type() !== String || customerID.type() !== String || clientID.type() !== String || clientSecret.type() !== String || username.type() !== String || password.type() !== String) {
let fields = [accountType, customerID, clientID, clientSecret, username, password];
test(fields);
}
accountType,
customerID,
clientID,
clientSecret,
username,
password;
}
const test = (array) => {
array.forEach((field) => {
let errorString;
if (field.type() !== String) {
errorString += `The field ${field} is not a String. Type: ${field}.`
}
return errorString;
})
}
//This const creates the auth URL and auth code grant URL. It also assigns the host URL.
const createAuthURL = (callback) => {
//If the paramter test passes then proceed
if (accountType === "Production") {
try {
let authURL = `https://${prodURL}${url1}`,
authCodeURL = `https://${prodURL}${url2}`,
host = prodURL;
} catch (e) {
console.log(`Error occurred when assigning url variables (Production). Error: ${e}`);
console.error('Error occurred when assigning url variables (Production). Error:', e);
}
} else if (accountType === "Internal") {
try {
let authURL = `https://${internURL}${url1}`,
authCodeURL = `https://${internURL}${url2}`,
host = internURL;
} catch (e) {
console.log(`Error occurred when assigning url variables (Internal). Error: ${e}`);
console.error('Error occurred when assigning url variables (Internal). Error:', e);
}
} else {
console.log(`Bad String Input into auth URL! Entry: ${accountType}`);
console.error('Bad String Input into auth URL: ', accountType);
}
}
const asyncWaterfall = (req, res) => {
//Asynchronous Waterfall Call of 'Getting Access Tokens' (Stages 1-3)
async.waterfall([
getCookieData,
getAuthCode,
getAccessToken
], (err, result) => {
if (err) {
alert('Something is wrong!');
}
return alert('Done!');
});
//STAGE 1 - USE CUSTOMER DETAILS TO RETRIEVE COOKIE AND CSRF SESSION DATA
getCookieData = async (callback) => {
//For testing purposes - Remove after development
console.log(`The type of account used is: ${accountType}`);
console.log(`The customerID used is: ${customerID}`);
console.log(`The clientID used is: ${clientID}`);
console.log(`The client secret used is: ${clientSecret}`);
console.log(`The username used is: ${username}`);
console.log(`The password used is: ${password}`);
console.log(`Making a URL request to: ${authURL}`);
//If the paramter test passes then proceed
var options = {
//Type of HTTP request
method: 'POST',
//URI String
uri: authURL,
//HTTP Query Strings
qs: {
'client_id': clientID
},
//HTTP Headers
headers: {
'cache-control': 'no-cache',
'Accept': 'application/json',
'Host': host,
'Content-Type': 'application/json'
},
//HTTP Body
body: {
'username': username,
'password': password
},
json: true // Automatically stringifies the body to JSON
//resolveWithFullResponse: true // Get the full response instead of just the body
//simple: false // Get a rejection only if the request failed for technical reasons
};
console.log(`Beginning HTTP POST Request to ${authURL}...`);
//await literally makes JavaScript wait until the promise settles, and then go on with the result.
await rp(options)
.then((parsedBody) => {
//POST Succeeded...
Console.log('Successful HTTP POST!');
try {
let csrf = response.body('csrftoken'),
ses = response.body('session'),
sesk = `session=${ses}`;
} catch (e) {
console.log(`STAGE 1 - Error occurred when assigning url variables. Error: ${e}`);
console.error('STAGE 1 - Error occurred when assigning url variables. Error:', e);
}
console.log(`Successful grab of the cookie: ${ses} and csrf token: ${csrf}. Getting authorisation code now!`);
//Asynchronous callback for the next function - return = defensive architecture
return callback(null, authCodeURL, customerID, clientID, csrf, sesk);
})
.catch((err) => {
if (res.statusCode == 400) {
console.log(`Error Message: ${res.body.message}. Status: ${res.body.status}`);
console.error('Error Message:', res.body.message, 'Status:', res.body.status);
} else if (res.statusCode == 401) {
console.log(`Error Message: ${res.body.message}. Status: ${res.body.status}`);
console.error('Error Message:', res.body.message, 'Status:', res.body.status);
} else {
console.log(`Failed to retrieve the cookie data! Error: ${error}`);
console.error('Failed to retrieve the cookie data! Error:', error);
}
});
},
//STAGE 2 - USE COOKIES AND CSRF TOKEN TO GET AUTH CODE
//Is the word async needed? it is not asyncchronous but sequential
getAuthCode = async (authCodeURL, customerID, clientID, csrf, sesk, callback) => {
//Make sure all the data is in string format - Run Time Tests
if (authCodeURL !== String || cutomerID !== String || clientID !== String || csrf !== String || sesk !== String) {
let fields = [authCodeURL, cutomerID, clientID, csrf, sesk];
test(fields);
}
//If successful, proceed:
var options = {
method: 'POST',
uri: authCodeURL,
qs: {
'client_id': clientID,
'response_type': 'code',
'scope': 'all'
},
headers: {
'X-CSRF-TOKEN': csrf,
'Accept': 'application/json',
'Cookie': sesk,
'Content-Type': 'application/json'
},
body: {
'customer_id': customerID
},
json: true // Automatically stringifies the body to JSON
};
console.log(`Beginning HTTP POST Request to ${authCodeURL}...`);
//await literally makes JavaScript wait until the promise settles, and then go on with the result.
await rp(options)
.then((parsedBody) => {
//POST Succeeded...
Console.log('Successful HTTP POST!');
try {
let authCode = response.body.JSON('auth_code'),
swapurl = `https://${host}${url3}`;
} catch (e) {
console.log(`STAGE 2 - Error occurred when assigning url variables. Error: ${e}`);
console.error('STAGE 2 - Error occurred when assigning url variables. Error:', e);
}
console.log(`The authorisation Code is ${authcode}. Getting Access Token now!`);
//Asynchronous callback for the next function - return = defensive architecture
return callback(null, swapURL, clientID, clientSecret, authCode);
})
.catch((err) => {
if (res.statusCode == 400) {
console.log(`Error Message: ${res.body.message}. Extra: ${res.body.extra}`);
console.error('Error Message:', res.body.message, 'Extra:', res.body.extra);
} else {
console.log(`Failed to retrieve the authorisation code! Error: ${error}`);
console.error('Failed to retrieve the authorisation code! Error: ', error);
}
});
},
//STAGE 3 - USE AUTH CODE TO GET ACCESS TOKEN
//ASYNC NEEDED?
getAccessToken = async (swapURL, clientID, clientSecret, authCode, callback) => {
//Make sure all the data is in string format - Run Time Tests
if (swapURL !== String || clientSecret !== String || clientID !== String || authCode !== String) {
let fields = [swapURL, clientSecret, clientID, authCode];
test(fields);
}
//If successful, proceed:
var options = {
method: 'POST',
uri: swapURL,
qs: {
'client_id': clientID,
'grant_type': 'authorization_code',
'client_secret': clientSecret,
'code': authCode
},
json: true // Automatically stringifies the body to JSON
};
console.log(`Beginning HTTP POST Request to ${swapURL}...`);
//await literally makes JavaScript wait until the promise settles, and then go on with the result.
await rp(options)
.then((parsedBody) => {
//POST Succeeded...
Console.log('Successful HTTP POST!');
try {
let accessToken = response.body('access_token'),
refreshToken = response.body('refresh_token');
} catch (e) {
console.log(`STAGE 3 - Error occurred when assigning url variables. Error: ${e}`);
console.error('STAGE 3 - Error occurred when assigning url variables. Error:', e);
}
console.log(`The access Token is ${accessToken} and the refreshToken which is ${refreshToken}! These are only valid for 2 hours!`);
//Asynchronous callback for the waterfall - return = defensive architecture
return callback(null, 'done');
})
.catch((err) => {
console.log(`Failed to retrieve the access/refresh Token! Error: ${error}`);
console.error('Failed to retrieve the access/refresh Token! Error:', error);
});
}
}