I'm testing out a route in a firebase app I'm building. The route in question recieves 'shouts' which are akin to status updates. Anyway, I just integrated auth to protect this route using FBuath, but I keep getting the following error:
Firebase ID token has expired. Get a fresh ID token from your client app and try again
I've tried relogging in using valid credentials, and then instantly trying to post something via the route, but keep getting this error. Any thoughts as to why? Code as follows, and the route in question has the endpoint '/shout'. Cheers
const functions = require('firebase-functions');
const admin = require('firebase-admin')
admin.initializeApp()
const config = {
apiKey: "AIzaSyBZjz9BNwj4UDwWLoQ1SOD5hB5QcNw3qqs",
authDomain: "social-ape-21874.firebaseapp.com",
databaseURL: "https://social-ape-21874.firebaseio.com",
projectId: "social-ape-21874",
storageBucket: "social-ape-21874.appspot.com",
messagingSenderId: "323044904203",
appId: "1:323044904203:web:edcbc619169a2087f8e60e",
measurementId: "G-T34PXDM1X7"
}
admin.initializeApp
const express = require('express')
const app = express()
const firebase = require('firebase')
firebase.initializeApp(config)
const db = admin.firestore()
app.get('/shouts', (req,res) => {
db
.collection('shouts')
.orderBy('createdAt', 'desc') //returns shouts in order in which they were made
.get()
.then((data) => {
let shouts = []
data.forEach((doc) => {
shouts.push({
shoutId: doc.id,
body: doc.data().body,
userHandle: doc.data().userHandle,
createdAt: doc.data().createdAt
})
})
return res.json(shouts)
})
.catch((err) => console.error(err))
})
const FBauth = (req,res,next) => {
let idToken
if(req.headers.authorization && req.headers.authorization.startsWith('Bearer ')){
idToken = req.headers.authorization.split('Bearer ')[1]
}else{
console.error('No token found')
return res.status(403).json({error: 'Unauthorized'})
}
//verify that this token was issued by our application
admin.auth().verifyIdToken(idToken)
.then(decodedToken => {
req.user = decodedToken
return db.collection('users')
.where('userId', '==', req.user.uid)
.limit(1) //limits results to one document
.get()
})
.then(data => {
req.user.handle = data.docs[0].data().handle //data() is a function that extracts data from document
return next() //next() is a function that allows request to proceed to shout post route
})
.catch(err => {
console.error('Error while verifying token', err)
return res.status(403).json(err)
})
}
app.post('/shout', FBauth, (req,res) => {
const newShout = {
body: req.body.body,
userHandle: req.body.userHandle, //userhandle identifies who is owner of shout
createdAt: new Date().toISOString()
}
db
.collection('shouts')
.add(newShout)
.then((doc) => {
res.json({message: `document ${doc.id} created successfully`})
})
.catch((err) =>{
res.status(500).json({error: 'something went wrong'})
console.error(err)
})
})
//helper function to determine if string is empty or not
//note: .trim() removes whitespace from email field
const isEmpty = (string) => {
if (string.trim()=== '') {
return true
} else {
return false
}
}
//helper function to determine if valid email
const isEmail = (email) => {
const regEx = /^(([^<>()\[\]\\.,;:\s#"]+(\.[^<>()\[\]\\.,;:\s#"]+)*)|(".+"))#((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
if (email.match(regEx)) {
return true
} else {
return false
}
}
//Sign up route
app.post('/signup', (req,res) => {
//here we need to extract form data from request body
const newUser = {
email: req.body.email,
password: req.body.password,
confirmPassword: req.body.confirmPassword,
handle: req.body.handle,
}
let errors = {}
if(isEmpty(newUser.email)){
errors.email = 'Email must not be empty'
} else if(!isEmail(newUser.email)) {
errors.email = 'Must be a valid email address'
} //if not empty, need to check if valid email
if(isEmpty(newUser.password)){
errors.password = 'Must not be empty'
}
if(newUser.password !== newUser.confirmPassword) {
errors.confirmPassword = 'Passwords must match'
}
if(isEmpty(newUser.handle)){
errors.handle = 'Must not be empty'
}
if(Object.keys(errors).length>0) {
return res.status(400).json(errors)
}
//sign up user
let token
db.doc(`/users/${newUser.handle}`).get()
.then((doc)=> {
if(doc.exists){
return res.status(400).json({handle: 'this handle is already taken'})
} else {
return firebase
.auth()
.createUserWithEmailAndPassword(newUser.email, newUser.password)
}
})
.then(data => {
userId = data.user.uid
return data.user.getIdToken()
})
.then(token => {
token=token
const userCredentials = {
handle: newUser.handle,
email: newUser.email,
createdAt: new Date().toISOString(),
userId:userId
}
db.doc(`/users/${newUser.handle}`).set(userCredentials)
return res.status(201).json({token})
})
.then(() => {
return res.status(201).json({token})
})
.catch(err => {
console.error(err)
return res.status(500).json({error:err.code})
})
})
//token is used to access route that is protected
//login route
app.post('/login', (req,res) => {
const user = {
email: req.body.email,
password: req.body.password
}
let errors = {}
if(isEmpty(user.email)){
errors.email = 'Must not be empty'
}
if(isEmpty(user.password)){
errors.password = 'Must not be empty'
}
if(Object.keys(errors).length >0) {
return res.status(400).json(errors)
}
firebase.auth().signInWithEmailAndPassword(user.email, user.password)
.then(data => {
return data.user.getIdToken()
})
.then(token => {
return res.json({token})
})
.catch(err => {
console.error(err)
if(err.code ==="auth/wrong-password" ){
return res.status(403).json({general: 'Wrong credentials, please try again'})
} else
return res.status(500).json({error: err.code})
})
})
exports.api = functions.https.onRequest(app)
Related
I tried to check JWT token worked actually or not. set in local storage. but after reloading, it set the original automatically. so here is any wrong or good suggestion?
front end:
in front end code is.
let userToken = localStorage.getItem("accessToken")
useEffect(() => {
axios.get(`http://localhost:5000/mycars/${user.uid}`,
{
headers: {
email: user.email,
token: userToken
}
})
.then(response => {
console.log(response);
if (response.data.success) {
setError("No car found")
}
else if (response.data.error) {
setError("Unauthorized access")
}
else {
setMycars(response.data)
setError("")
}
})
.catch(err => {
console.log("error is ", err);
})
}, [user.uid, user.email, userToken])
backend:
app.get('/mycars/:id', async (req, res) => {
const uid = req.params.id;
const getEmail = req.headers.email;
const accessToken = req.headers.token;
try {
const decoded = await jwt.verify(accessToken, process.env.DB_JWTTOKEN,
function (err, decoded) {
let email;
if (err) {
email = "invalid email"
}
if (decoded) {
email = decoded.email
}
return email;
});
// console.log(getEmail, decoded);
if (getEmail === decoded) {
const query = {}
const cursor = carCollection.find(query);
const cars = await cursor.toArray();
const mycars = await cars.filter(car => car.uid === uid)
if (mycars.length === 0) {
res.send({ success: "No car found" })
}
else {
res.send(mycars)
}
}
else {
res.send({ error: "Unauthorized access" })
}
}
catch (err) {
}
with the code , I wanted to check the JWT token manually changing in local storage. when page reloaded, the token execute new. and the edited not remaining
I didn't initially get this error, but it mysteriously appeared later on in my code. I tried following allong with the firebase documentation and using the auth.getAuth() method but then got the following error:
** : TypeError: auth.getAuth(...).verifyIdToken is not a function **
This is my auth code:
//const { auth } = require('firebase-admin');
const { admin, db } = require('./admin');
//1. ******************
//const auth = require('firebase/auth');
module.exports = (req, res, next) => {
let idToken;
if(req.headers.authorization && req.headers.authorization.startsWith('Bearer ')){
idToken = req.headers.authorization.split('Bearer')[1]
//returns an array of 2 strings --> 2nd element is the token
}
else{
console.error("NO TOKEN FOUND");
return res.status(403).json({error: "Unauthorized"});
}
admin.auth().verifyIdToken(idToken)
.then(decodedToken => {
req.user = decodedToken;
console.log(decodedToken);
return db.collection('users')
.where('userId', '==', req.user.uid)
.limit(1)
.get();
})
.then(data => {
req.user.handle = data.docs[0].data().handle;
return next();
})
.catch(err => {
console.error('Error while verifying token', err);
return res.status(403).json(err);
})
}
** where admin is require('firebase-admin');
I tried applying the auth.getAuth() method in the documentation, but my postman response says it is not a function. I'm very lost right now, any help would be very appreciated.
This is the full error:
{ "code": "auth/argument-error",
"message": "Decoding Firebase ID token failed. Make sure you passed the entire string JWT which represents an ID token. See https://firebase.google.com/docs/auth/admin/verify-id-tokens for details on how to retrieve an ID token."
}
Here is my users.js that the id token gets to:
const { admin, db } = require("../util/admin");
const config = require('../util/config')
const firebase = require('firebase/app')
firebase.initializeApp(config)
const { validateSignUpData, validateLoginData, reduceUserDetails} = require('../util/validators')
const auth = require('firebase/auth');
// const { user } = require("firebase-functions/v1/auth");
exports.signup = (req,res) => {
const newUser = {
email:req.body.email,
password:req.body.password,
confirmPassword:req.body.confirmPassword,
handle:req.body.handle,
};
const { valid, errors } = validateSignUpData(newUser);
if(!valid) return res.status(400).json(errors);
//when a user signs up give them a blank image
const noImg = 'blankpic.png';
//declare an errors object as an empty object
let token, userId;
db.doc(`/users/${newUser.handle}`).get()
.then((doc) => {
if(doc.exists){
return res.status(400).json({ handle: 'this handle is already taken'})
} else{
return auth.createUserWithEmailAndPassword(auth.getAuth(), newUser.email,newUser.password)
}
})
.then((data) => {
//return access token for user to req more data
//return data.user.getIdToken();
userId = data.user.uid;
return data.user.getIdToken();
})
.then((idToken) => {
token = idToken;
//create user document
const userCredentials = {
handle: newUser.handle,
email: newUser.email,
createdAt: new Date().toISOString(),
imageUrl: `https://firebasestorage.googleapis.com/v0/b/${config.storageBucket}/o/${noImg}?alt=media`,
userId
};
//persist document into users cllection
return db.doc(`/users/${newUser.handle}`).set(userCredentials);
//return res.status(201).json( { token } );
})
.then(() => {
return res.status(201).json({ token })
})
.catch((err) => {
console.error(err);
if (err.code == 'auth/email-already-in-use') {
return res.status(400).json({ email: 'Email is already in use'})
}
return res.status(500).json({ general: "Something went wrong with server; please try again"});
});
}
exports.login = (req,res) => {
const user = {
email: req.body.email,
password: req.body.password
};
const { valid, errors } = validateLoginData(user);
if(!valid) return res.status(400).json(errors);
auth.signInWithEmailAndPassword(auth.getAuth(), user.email, user.password)
.then(data => {
return data.user.getIdToken();
})
.then(token => {
return res.json({ token });
})
.catch((err) => {
console.error(err);
return res
.status(403).json({general: 'Wrong credentials, please try again'});
});
}
exports.addUserDetails = (req,res) => {
let userDetails = reduceUserDetails(req.body);
db.doc(`/users/${req.user.handle}`).update(userDetails)
.then(() => {
return res.json({ message: 'Details add successfully'});
})
.catch(err => {
console.error(err);
return res.status(500).json({error: err.code});
})
}
exports.getAuthenticatedUser = (req,res) => {
let userData = {};
db.doc(`/users/${req.user.handle}`).get()
.then(doc => {
if(doc.exists){
userData.credentials = doc.data();
return db.collection('likes').where('userHandle', '==', req.user.handle).get()
}
})
.then(data => {
userData.likes = [];
data.forEach(doc => {
userData.likes.push(doc.data());
});
//return res.json(userData);
return db.collection('notifications').where('recipient', '=='. req.user.handle).get();
// .orderBy('createdAt').limit(10).get();
})
.then(data => {
userData.notifications = [];
data.forEach(doc => {
userData.notifications.push({
//recipient: doc.data().recipient,
sender: doc.data().sender,
createdAt: doc.data().createdAt,
screamId: doc.data().screamId,
type: doc.data().type,
read: doc.data().read,
notificationId: doc.id
});
});
return res.json(userData);
})
.catch(err => {
console.error(err);
return res.status(500).json({ error: err.code });
})
}
exports.getUserDetails = (req, res) => {
let userData = {};
db.doc(`/users/${req.params.handle}`)
.get()
.then((doc) => {
if (doc.exists) {
userData.user = doc.data();
return db
.collection("critech")
.where("userHandle", "==", req.params.handle)
//.orderBy("createdAt", "desc")
.get();
} else {
return res.status(404).json({ errror: "User not found" });
}
})
.then((data) => {
userData.posts = [];
data.forEach((doc) => {
userData.posts.push({
body: doc.data().body,
createdAt: doc.data().createdAt,
userHandle: doc.data().userHandle,
userImage: doc.data().userImage,
likeCount: doc.data().likeCount,
commentCount: doc.data().commentCount,
screamId: doc.id,
});
});
return res.json(userData);
})
.catch((err) => {
console.error(err);
return res.status(500).json({ error: err.code });
});
};
exports.uploadImage = (req, res) => {
const busboyCons = require("busboy");
const path = require("path");
const os = require("os");
const fs = require("fs");
var busboy = busboyCons({ headers: req.headers });
let imageToBeUploaded = {};
let imageFileName;
// String for image token
//let generatedToken = uuid();
busboy.on("file", (fieldname, file, filename, encoding, mimetype) => {
console.log(fieldname, filename, mimetype);
// if (mimetype !== "image/jpeg" && mimetype !== "image/png") {
// return res.status(400).json({ error: "Wrong file type submitted" });
// }
// my.image.png => ['my', 'image', 'png']
filename = filename.toString();
const imageExtension = filename.split(".")[filename.split(".").length - 1];
// 32756238461724837.png
imageFileName = `${Math.round(
Math.random() * 1000000000000
).toString()}.${imageExtension}`;
//tmdir() --> temporarydirectory
const filepath = path.join(os.tmpdir(), imageFileName);
imageToBeUploaded = { filepath, mimetype };
//use file system lib to create the file object using the node js function file.pipe
file.pipe(fs.createWriteStream(filepath));
});
busboy.on("finish", () => {
admin
.storage()
.bucket()
.upload(imageToBeUploaded.filepath, {
resumable: false,
metadata: {
metadata: {
contentType: imageToBeUploaded.mimetype,
//Generate token to be appended to imageUrl
// firebaseStorageDownloadTokens: generatedToken,
},
},
})
.then(() => {
//construct image url to add to our user
// Append token to url
const imageUrl = `https://firebasestorage.googleapis.com/v0/b/${config.storageBucket}/o/${imageFileName}`;
return db.doc(`/users/${req.user.handle}`).update({ imageUrl });
})
.then(() => {
return res.json({ message: "image uploaded successfully" });
})
.catch((err) => {
console.error(err);
return res.status(500).json({ error: "something went wrong" });
});
});
busboy.end(req.rawBody);
};
exports.markNotificationsRead = (req, res) => {
let batch = db.batch();
req.body.forEach((notificationId) => {
const notification = db.doc(`/notifications/${notificationId}`);
batch.update(notification, { read: true });
});
batch
.commit()
.then(() => {
return res.json({ message: "Notifications marked read" });
})
.catch((err) => {
console.error(err);
return res.status(500).json({ error: err.code });
});
};
This is the token I get back when I log in 2
The response I get back when I use the token in my authorization header in Postman 1
The authorization header is of format Bearer <Token> with a space in between. However you are passing 'Bearer' in split() which would result in ['', ' <token>'] (notice the additional whitespace before actual token). You must use .split(" ") and this should resolve it. Try refactoring the code as shown below:
if( req.headers.authorization && req.headers.authorization.startsWith('Bearer ')) {
idToken = req.headers.authorization.split(' ')[1]
}
const r1 = "Bearer token".split("Bearer")
const r2 = "Bearer token".split(" ")
console.log(`Token 1: '${r1[1]}'`)
console.log(`Token 2: '${r2[1]}'`)
when i try changing password using postman everything works but when i try to do that on the frontend i don't get any errors on the backend, the password isn't changed and i get error on network tab 'id is not valid'.
here is my backend:
routes:
router.post("/reset", reset);
router.post("/reset/:userId/:token", changepw);
controllers:
export const reset = async (req, res) => {
const { email } = req.body;
try {
if (validateEmail(email) === false)
return res.status(400).json({ error: "Please enter a valid email" });
const user = await User.findOne({ email });
if (!user) return res.status(404).json({ error: "User doesn't exist" });
let token = await Token.findOne({ userId: user._id });
if (!token) {
token = await new Token({
userId: user._id,
token: crypto.randomBytes(32).toString("hex"),
}).save();
}
const link = `http://localhost:3000/auth/reset/${user._id}/${token.token}`;
// when the port is 5000 here its works on postman but when i set 3000 it doesn't work
const result = await sendEmail(user.email, link);
res.status(200).json({ result, token });
} catch (error) {
res.status(500).json({ error: "Something went wrong" });
console.log(error);
}
};
export const changepw = async (req, res, next) => {
const { password, confirmPassword } = req.body;
const { userId: _id, token } = req.params;
try {
if (!mongoose.Types.ObjectId.isValid(_id))
return res.status(400).json({ error: "Id is not valid" });
const user = await User.findById(_id);
if (!user)
return res.status(400).json({ error: "Invalid link or expired" });
const tokens = await Token.findOne({
userId: user._id,
token,
});
if (!tokens)
return res.status(400).json({ error: "Invalid link or expired" });
if (password !== confirmPassword)
return res.status(400).json({ error: "Passwords don't match" });
const hashedPassword = await bcrypt.hash(password, 12);
const result = await User.findByIdAndUpdate(
_id,
{ password: hashedPassword },
{ new: true }
);
await tokens.delete();
res.status(200).json({ result, tokens });
} catch (error) {
res.status(500).json({ error: "Something went wrong" });
console.log(error);
next(error);
}
};
here is the frontend:
this is how i connect backend and frontend:
export const reset = (email) => API.post("/auth/reset", email);
export const changepw = (pw) => API.post("/auth/reset/:userId/:token", pw);
<Route path="/auth/reset/:userId/:token" component={ChangePassword} />
// this the component where user go to change password
redux actions:
export const reset = (email) => async (dispatch) => {
try {
const { data } = await api.reset(email);
dispatch({ type: actionTypes.RESET, data });
} catch (error) {
const err = JSON.stringify(error?.response?.data);
sessionStorage.setItem("error", err);
}
};
export const changepw = (pw) => async (dispatch) => {
try {
const { data } = await api.changepw(pw);
dispatch({ type: actionTypes.CHANGEPW, data });
} catch (error) {
const err = JSON.stringify(error?.response?.data);
sessionStorage.setItem("error", err);
}
};
reducers:
export default (state = { user: null, link: null }, action) => {
switch (action.type) {
case actionTypes.RESET:
return { ...state, link: action.payload };
case actionTypes.CHANGEPW:
return { ...state, user: [state.user, action.payload] };
default:
return state;
}
};
this is what i do when user submit new password:
const [data, setData] = useState({
password: "",
confirmPassword: "",
});
const handleSubmit = async (e) => {
e.preventDefault();
dispatch(changepw({ ...data }));
setData({
password: "",
confirmPassword: "",
});
// history.push("/auth");
};
I have this createUser method:
const createUser = (request, response) => {
const { username, password, score } = request.body
database("user")
.select()
.where({ username })
.first()
.then(user => {
if (!user) {
return bcrypt.hash(password, 12)
.then(hashedPassword => {
return database("user").insert({
username, password_digest: hashedPassword, score
}).returning("*")
})
.then(users => {
const secret = "HERESYOURTOKEN"
jwt.sign(users[0], secret, (error, token) => {
console.log(users[0])
response.json({ token, user: users[0] })
})
})
}
response.send("Please choose another username")
}).catch(error => {
response.status(401).json({
error: error.message
})
})
}
And this Authentication method:
function authenticate(request, response, next){
const token = request.headers.authorization.split(" ")[1]
const secret = "HERESYOURTOKEN";
if (!token) {
response.sendStatus(401)
}
let id = null
try {
id = jwt.verify(token, secret)
} catch(error){
response.sendStatus(403)
}
const user = database("user")
.select()
.where("id", id)
.first()
request.user = user;
next();
}
And when I use the Postman to create a new user it gives me 401 Unauthorized error with message:
enter image description here
The score data type is integer:
exports.up = function(knex) {
return knex.schema.createTable("user", table => {
table.increments()
table.string("username")
table.string("password_digest")
table.integer("score")
})
};
exports.down = function(knex) {
return knex.schema.dropTableIfExists("user")
};
Probably someone can help me with it.
The error should be from jwt.sign() where your secret is not in a right format.
Can you try a simple const secret = "ABC" to see if it works?
I have a function to signup users in my express API. Please have a look at signup route:
//Route to SIGN UP
app.post('/signup', (req, res) => {
const newUser = {
email : req.body.email,
password : req.body.password,
confirmPassword: req.body.confirmPassword,
handle: req.body.handle
}
let errors = {};
if(isEmpty(newUser.email)) errors.email = 'Must not be empty';
else if (!isEmail(newUser.email)) errors.email = 'Must be a valid email address';
if(isEmpty(newUser.password)) errors.password = 'Must not be empty';
if(newUser.password !== newUser.confirmPassword) errors.confirmPassword = 'Passwords must match';
if(isEmpty(newUser.handle)) errors.handle = 'Must not be empty';
if(Object.keys(errors).length > 0) return res.status(400).json(errors);
let token, userId;
db.doc(`/users/${newUser.handle}`).get()
.then(doc => {
if(!doc.exists) {
return firebase.auth().createUserWithEmailAndPassword(newUser.email, newUser.password)
} else {
return res.status(400).json({ handle: 'this handle already exists'})
}
})
.then(data => {
userId = data.user.uid;
return data.user.getIdToken()
})
.then(idToken => {
token = idToken;
const userCredential = {
handle: newUser.handle,
email: newUser.email,
createdAt: new Date().toISOString(),
userId
}
db.doc(`/users/${newUser.handle}`).set(userCredential);
})
.then ( () => {
return res.status(201).json({ token })
})
.catch(err => {
if(err.code === 'auth/email-already-in-use') {
return res.status(400).json({ email: 'Email is already in use'})
} else {
return res.status(500).json(console.log(err));
}
}
)
})
//end of SIGN UP
Postman API doesn't show anything, if I send some JSON message in my last else statement, that gets shown up in the postman.
It shows me error in powershell when I run => firebase serve
i functions: Beginning execution of "api"
Error: Could not load the default credentials. Browse to https://cloud.google.com/docs/authentication/getting-started for more information.
at GoogleAuth.getApplicationDefaultAsync (D:\Projects\socialape\functions\node_modules\google-auth-library\build\src\auth\googleauth.js:161:19)
at process._tickCallback (internal/process/next_tick.js:68:7)
i functions: Finished "api" in ~1s