Using Firebase Auth with an external database - node.js

I'm currently using Firebase to authenticate my users in a React/Node app, but I also want to store additional user data in my own database and I'm doing so by storing the Firebase uid on each user and I wanted to get some input on my implementation to make sure I'm on the right track.
My frontend code is as follows:
This is used as an onClick on a "Continue with Google" button:
const googleSignIn = async () =>
signInWithPopup(auth, new GoogleAuthProvider());
When the above popup promise completes, auth.onAuthStateChanged is triggered in the following useEffect, which (on login/signup) would trigger the function applicationAuthentication, passing in the user object returned from Firebase:
useEffect(() => {
const unsubscribe = auth.onAuthStateChanged(firebaseUser => {
if (!firebaseUser) {
return dispatch(logUserOut());
}
return applicationAuthentication(firebaseUser);
});
return unsubscribe;
}, []);
The applicationAuthentication looks as follows:
const applicationAuthentication = async (firebaseUser: User) => {
try {
const idToken = await firebaseUser.getIdToken();
const { data } = await axios.get('/api/users/authenticate/signin', {
headers: {
Authorization: `Bearer ${idToken}`
}
});
const { user, error } = data;
if (error) {
throw new Error(error.message);
}
dispatch(logUserIn({ user, accessToken: idToken }));
} catch (error: any) {
dispatch(setUserError(error.message));
console.log(error.message);
}
};
In my node express server, the following happens at the route /api/users/authenticate/signin; this is where I communicate with my own database by using the data access methods findUserByFirebaseUID and createUser using the uid from the token to check if the user exists, and if not, creating a new one (note the middleware that's checked first as noted below):
usersRouter.get(
'/authenticate/signin',
async (req: Request, res: Response, next: NextFunction) => {
try {
const uid = res.locals.uid; // set by token middlewear function
let firstLogin = false;
let user = await findUserByFirebaseId(uid);
if (!user) {
firstLogin = true;
user = await createUser(uid);
}
res.json({ user, firstLogin });
} catch (error) {
next(error);
}
}
);
Which uses the following authenticate middleware function to authenticate the user with firebase-admin:
const authenticate = async (
req: Request,
res: Response,
next: NextFunction
) => {
try {
const idToken = req.headers.authorization.split(' ')[1];
const decodedToken = await admin.auth().verifyIdToken(idToken);
if (decodedToken) {
const { uid } = decodedToken;
res.locals.uid = uid;
return next();
}
return res.status(400).json({ message: 'Unauthorized Request' });
} catch (error) {
next({ message: 'Invalid Token' });
}
};
app.use(authenticate);
Does this overall flow of using the uid to check my own database seem correct? And am I implementing the token middleware correctly?
I'd love to hear any thoughts on this!

Yes, the way you pass the ID token from the client to the server, and then decode it (in the middleware) on your server to securely determine the UID is similar to how Firebase's own services do this.
If you pass the ID token to other requests to to authorize them, consider keeping a cache of recent raw and decoded ID tokens, to prevent having to decode them on each request.

Related

What is the best practice for using routes and controllers in a Node.js Express application?

What is the best practice for using routes and controllers in a Node.js Express application, specifically with regards to managing user and shard data in the application? Currently, I am checking req.user and req.shard in each controller and passing them as arguments to each service method. Is there a way to avoid this and have the user and shard data accessible in the service without having to pass it as an argument each time?
Routes:
router.put(
'/change',
accessMiddleware([
UserRoleEnum.Owner,
UserRoleEnum.Manager,
]),
qrController.change,
);
Controller:
export const change = async (
req: Request,
res: Response,
next: NextFunction,
) => {
if (!req.user || !req.user.shard)
throw new CommonError(ErrorEnum.Unauthorized);
const user = req.user;
const shard = req.user.shard;
const changeRequest: changeRequest = req.body as ChangeRequest;
const session = await mongoose.startSession();
session.startTransaction();
try {
const result = await service.change(session, shard, changeRequest);
await session.commitTransaction();
session.endSession();
return res.json({
status: 'success',
msg: 'Changed',
code: 'alert.changed',
data: result,
});
} catch (error) {
await session.abortTransaction();
session.endSession();
next(error);
}
};

In React 18, when I am sending data to nodejs server, it is sending two request and receiving two response from server

This is the front-end code which is used for sending access token to server site.
useEffect(() => {
const getProducts = async () => {
try {
const url = `http://localhost:5000/product?email=${user.email}`
const { data } = await axios.get(url, {
headers: {
authorization: localStorage.getItem('accessToken')
}
});
setProducts(data);
} catch (err) {
const status = err.response.status;
if (status === 401 || status === 403) {
signOut(auth);
navigate('/login');
localStorage.removeItem('accessToken')
toast.error(err.response?.data?.message);
}
}
}
getProducts();
}, [user.email]);
This is server site express code for response. Why every time it is receiving two request and sending two response?
app.get('/product', verifyToken, async (req, res) => {
const decoded = req.decoded?.email;
const queryEmail = req.query?.email;
if (decoded === queryEmail) {
const query = { email: queryEmail };
const cursor = medicineCollection.find(query);
const products = await cursor.toArray();
res.send(products);
} else {
res.status(403).send({ message: "Forbidden Access" })
}
})
Maybe you take user.email in a state which is updating somehow so that's why useEffect is calling again and giving you twice response.

.save() is not a function (Mongoose)

So, I'm not 100% why this isn't working as intended. I have an Edit Profile React component (I'm learning how to build a SSR-based application currently, using the MERN stack) - but when I submit the edit, I get an error that "user.save is not a function - Code:
From the routes:
router.route('/api/users/:userId')
.get(authCtrl.requireSignin, userCtrl.read)
.put(authCtrl.requireSignin, authCtrl.hasAuthorization, userCtrl.update)
.delete(authCtrl.requireSignin, authCtrl.hasAuthorization, userCtrl.remove)
The API Helper:
const update = async (params, credentials, user) => {
try {
let response = await fetch('/api/users/' + params.userId, {
method: 'PUT',
headers: {
"Accept": 'application/json',
Authorization: 'Bearer ' + credentials.t
},
body: user
})
return await response.json()
} catch (err) {
console.log(err)
}
}
And lastly, the actual controller, that handles all the logic behind the update: (This function sanitizes the password information before passing it back to the client, hence the undefineds)
const update = (req, res) => {
let form = new formidable.IncomingForm()
form.keepExtensions = true
form.parse(req, async (err, fields, files) => {
if (err) {
return res.status(400).json({
error: "Photo could not be uploaded"
})
}
let user = req.profile
user = extend(user, fields)
user.updated = Date.now()
if(files.photo){
user.photo.data = fs.readFileSync(files.photo.path)
user.photo.contentType = files.photo.type
}
try {
await user.save()
user.hashed_password = undefined
user.salt = undefined
res.json(user)
} catch (err) {
console.log(err)
return res.status(400).json({
error: errorHandler.getErrorMessage(err)
})
}
})
}
This isn't a production level application, just for me learning how to do this from scratch (without CRA, and all contained in one project using SSR)
EDIT: After some digging, console.logs and console.dirs, I discovered that the updates passed from the component aren't even being passed to the controller. The stale data (from the database) are logging, but req.profile is completely empty. I may re-visit this code completely and make some major changes to it.. All part of learning, right?
Here are the auth methods that were requested (I'm using Session Storage for now, but that may change to localStorage):
import User from '../models/user.model'
import jwt from 'jsonwebtoken'
import expressJwt from 'express-jwt'
import config from './../../config/config'
const signin = async (req, res) => {
try {
let user = await User.findOne({email: req.body.email})
if (!user) {
return res.status(401).json({error: "User not found"})
}
if (!user.authenticate(req.body.password)) {
return res.status(401).send({error: "Email and Password do not match"})
}
const token = jwt.sign({_id: user._id}, config.jwtSecret)
res.cookie('t', token, {expire: new Date() + 9999})
return res.json({
token,
user: {
_id: user._id,
name: user.name,
email: user.email
}
})
} catch (err) {
return res.status(401).json({error: "Could not sign in"})
}
}
const signout = (req, res) => {
res.clearCookie('t')
return res.status(200).json({message: "Signed out"})
}
const requireSignin = expressJwt({
secret: config.jwtSecret,
algorithms: ['sha1', 'RS256', 'HS256'],
userProperty: 'auth'
})
const hasAuthorization = (req, res, next) => {
const authorized = req.profile && req.auth
&& req.profile._id == req.auth._id
if (!(authorized)) {
return res.status(403).json ({error: "User is not authorized"})
}
next()
}
export default {
signin,
signout,
requireSignin,
hasAuthorization
}
Possible places where you could have a mistake: (code is not shown)
If your req.profile isn't a mongoose object, this won't work
let user = req.profile
From your other posts, I think you're probably getting req.profile from your jwt. That means this is not a mongoose object. What you'll need to do is either:
As you mentioned, use findByIdAndUpdate passing the id and the object to be updated. Note that if you have a mongoose middleware for save it won't run here
Do a user = await User.findById(id), update the user as you see fit, then use user.save. This gives you a bit more control over it, but runs 2 operations.
This has been solved.. My issue was apparently with the form not passing the request body properly to the API, which was caused by a faulty install of a dependency. Once I got that solved, the rest fell into place, and I can now do what I need to do with ease..
Thank you all who attempted to troubleshoot this with me.

How i can return the token after the user register complete?

I'm making a api to register users and i like to return in the json response the user and the jwt token.
Actually this is my function:
initializeCreate( {request} ){
const data = request.only(["username", "password", "permission", "status"])
return new Promise(function(resolve, reject) {
user.create(data, function(err, resp, body) {
if (err) {
reject(err);
} else {
resolve(JSON.parse(body))
}
})
})
}
createUser({ auth }){
var initializePromise = initializeCreate();
initializePromise.then(function(result) {
const token = await auth.attempt(result.username, result.password)
return token
}, function(err) {
console.log(err);
})}
I suppose that i have to wait the event User.create() finish to make the auth.attempt, so i create this promise, but this is the better way to do this? There's a way that i make this in only 1 function?
Actually i'm receiving this error:
Unexpected token const token = await auth.attempt(result.username,
result.password)
You can use .generate(user) - documentation
const user = await User.find(1)
const token = await auth.generate(user)
or .attempt(uid, password) - documentation
const token = await auth.attempt(uid, password)
I suppose that i have to wait the event User.create() finish to make
the auth.attempt, so i create this promise, but this is the better way
to do this?
Yes. You need to use await. like :
await user.create(...)
(And now you can put everything in a function)
You can use try/catch :
async login({ auth, response, request }) {
const data = request.only(['email', 'password'])
try {
await auth.check()
return response.status(200).send({ message: 'You are already logged!' })
} catch (error) {
try {
return await auth.attempt(data.email, data.password)
} catch (error) {
return response.status(401).send({ message: error.message })
}
}
}
Sample code for a personal project

Endpoints not authenticating with Fetch API calls (using passport-google-oauth2)

I have passport set up to use the Google strategy and can direct to the /auth/google great. I currently have it so that when you log in using the google authentication oauth2, my endpoints will authenticate by checking for a req.user. This works when I'm just getting to the endpoints in my browser. If I go to /auth/google and then /questions, I'll be able to make that get request. However when I try to make a fetch request from redux, I will get an error message saying Uncaught (in promise) SyntaxError: Unexpected token < in JSON at position 0. It comes up because the fetch API tries to get to my /questions endpoint, goes through my loggedIn middleware and then doesn't meet the if (!req.user) and gets re-directed instead. Any ideas on how to authenticate from the Fetch API with PassportJS and passport-google-oauth2?
The loggedIn function:
function loggedIn(req, res, next) {
if (req.user) {
next();
} else {
res.redirect('/');
}
}
Here is my code for my 'GET' endpoint.
router.get('/', loggedIn, (req, res) => {
const userId = req.user._id;
User.findById(userId, (err, user) => {
if (err) {
return res.status(400).json(err);
}
Question.findById(user.questions[0].questionId, (err, question) => {
if (err) {
return res.status(400).json(err);
}
const resQuestion = {
_id: question._id,
question: question.question,
mValue: user.questions[0].mValue,
score: user.score,
};
return res.status(200).json(resQuestion);
});
});
});
The redux fetch request:
function fetchQuestion() {
return (dispatch) => {
let url = 'http://localhost:8080/questions';
return fetch(url).then((response) => {
if (response.status < 200 || response.status >= 300) {
let error = new Error(response.statusText);
error.response = response;
throw error;
}
return response.json();
}).then((questions) => {
return dispatch(fetchQuestionsSuccess(questions));
}).catch((error) => {
return dispatch(fetchQuestionsError(error));
}
};
}
The Fetch API doesn't send cookies by default, which Passport needs to confirm the session. Try adding the credentials flag to ALL your fetch requests like so:
fetch(url, { credentials: 'include' }).then...
or if you aren't doing CORS requests:
fetch(url, { credentials: 'same-origin' }).then...

Resources