I am having a slight problem with authorizing the admin.
The backend code works, but i have got problems with requesting the admin route and authenticating the logged in user. First thing i have tried was to put the isAdmin value in the cookies, but it wasnt secure. Then i tried to verify the admin with cookies, i used cookie.get() to get the token. But it was not a success.
code Authorization:
const isAdmin = async (req, res, next) => {
if (!req.user.isAdmin) {
res.status(401).send({ msg: "Not an authorized admin" });
} else {
res.send(req.user.isAdmin);
// const token = req.header("auth-token");
// const verified = verify(token, process.env.SECRET);
// req.user = verified;
// next();
}
next();
};
code Admin route:
router.get("/adminPanel", isAuth, isAdmin, (req, res) => {});
code Login page:
const handleSubmit = e => {
e.preventDefault();
Axios.post("http://localhost:5000/users/login", {
email,
password,
})
.then(response => {
cookie.set("token", response.data.token, {
expires: 1,
});
setUser({
token: response.data.token,
});
if (response.data.isAdmin) {
alert("admin");
} else {
alert("not an admin");
}
// console.log(response.data.token);
// console.log(response.data.isAdmin);
})
.catch(err => {
console.log(err);
});
};
code Admin page:
import React, { useContext, useEffect, useState } from "react";
import Axios from "axios";
import { userContext } from "../../App";
export default function Home() {
const [user, setUser] = useContext(userContext);
const [content, setContent] = useState("login plz to display the content");
useEffect(() => {
// Axios.get("http://localhost:5000/users/adminPanel").then(response =>
// console.log(response.data),
// );
// async function fetchAdmin() {const result = await
Axios.get("http://localhost:5000/users/adminPanel", {
headers: {
Authorization: `Bearer ${user.isAdmin}`,
},
});
// }
// fetchAdmin();
// async function fetchProtected() {
// const result = await (
// await fetch("http://localhost:5000/users/adminPanel", {
// method: "GET",
// headers: {
// "Content-Type": "application/json",
// authorization: `Bearer ${user.token}`,
// },
// })
// ).json();
// if (result.isAdmin) setContent("Admin");
// }
// fetchProtected();
}, [user]);
return `${content}`;
}
Getting the token from cookies:
const [user, setUser] = useState({});
useEffect(() => {
setUser({ token: cookie.get("token") });
}, []);
console.log(user);
Taking into account your route
router.get("/adminPanel", isAuth, isAdmin, (req, res) => {});
I assume req.user.isAdmin is set in isAuth middleware, so your isAdmin middleware should check that parameter, let it pass if so, or reject it otherwise.
In the isAuth middleware after you validate the user, you should know if is an admin or not, so just set the parameter like this:
const isAuth = async (req, res, next) => {
// other code
req.user.isAdmin = true // put your logic here to reflect the status
next(); // pass the control to next middleware, in your example to isAdmin
}
Finally isAdmin could look like this:
const isAdmin = async (req, res, next) => {
if (!req.user.isAdmin) {
res.status(401).send({ msg: "Not an authorized admin" });
} else {
next();
}
};
Related
I am using firebase for authentication in my Next.js app and also I have an express server that serves a REST API, which has a middleware that uses firebase-admin to verify idToken that is sent from my app, to pass the authenticated routes
Currently
The idToken generated by firebase lasts for one hour and if the client is still on my app and
hits any route that needs idToken and if the idToken is expired then the server just throws an error as unauthenticated, which is pretty good work, but this is not desired, I know my user is in there and just idToken is expired
Question
How do I refresh my idToken of a user if it has expired, without having to do a full refresh in the browser to get new idToken
Some Code
AuthContext.tsx
/* eslint-disable no-unused-vars */
import { useRouter } from 'next/router'
import nookies from 'nookies'
import { createContext, useContext, useEffect, useState } from 'react'
import { axios } from '../config/axios'
import firebase from '../config/firebase'
import { AuthUser } from '../types'
import { BaseUser } from '../types/user'
import { getProvider } from '../utils/oAuthProviders'
type AuthContextType = {
user: AuthUser | null
login: (email: string, password: string) => Promise<any>
signup: (email: string, password: string) => Promise<any>
logout: () => Promise<any>
oAuthLogin: (provider: string) => Promise<any>
}
const AuthContext = createContext<AuthContextType>({} as AuthContextType)
export const useAuth = () => useContext(AuthContext)
const fromPaths = ['/login', '/signup']
const formatUser = (user: BaseUser, idToken: string): AuthUser => {
return {
...user,
idToken,
}
}
export const AuthContextProvider = ({ children }: { children: React.ReactNode }) => {
const [user, setUser] = useState<AuthUser | null>(null)
const [loading, setLoading] = useState(true)
const router = useRouter()
console.log(user)
useEffect(() => {
const unsub = firebase.auth().onIdTokenChanged((user) => {
if (user) {
user
.getIdToken()
.then(async (idToken) => {
try {
const userResp = await axios.get('/user/me', {
headers: {
Authorization: `Bearer ${idToken}`,
},
})
nookies.set(undefined, 'idk', idToken, { path: '/' })
const {
data: { userFullDetials },
} = userResp
setUser(formatUser(userFullDetials, idToken))
setLoading(false)
if (fromPaths.includes(router.pathname)) {
router.push('/home')
}
} catch (err) {
console.log(err)
setUser(null)
setLoading(false)
}
})
.catch((err) => {
console.log(err.message)
setUser(null)
setLoading(false)
})
} else {
setLoading(false)
setUser(null)
}
})
return () => unsub()
}, [router])
const login = (email: string, password: string) => {
return firebase.auth().signInWithEmailAndPassword(email, password)
}
const signup = (email: string, password: string) => {
return firebase.auth().createUserWithEmailAndPassword(email, password)
}
const oAuthLogin = (provider: string) => {
return firebase.auth().signInWithPopup(getProvider(provider))
}
const logout = async () => {
setUser(null)
await firebase.auth().signOut()
}
const returnObj = {
user,
login,
signup,
logout,
oAuthLogin,
}
return (
<AuthContext.Provider value={returnObj}>
{loading ? (
<div className="flex items-center justify-center w-full h-screen bg-gray-100">
<h1 className="text-indigo-600 text-8xl">S2Media</h1>
</div>
) : (
children
)}
</AuthContext.Provider>
)
}
// auth.ts
// Auth Middleware in express
import { NextFunction, Request, Response } from 'express'
import fbadmin from 'firebase-admin'
import { DecodedIdToken } from '../types/index'
export default async (req: Request, res: Response, next: NextFunction) => {
const authorization = req.header('Authorization')
if (!authorization || !authorization.startsWith('Bearer')) {
return res.status(401).json({
status: 401,
message: 'authorization denied',
})
}
const idToken = authorization.split(' ')[1]
if (!idToken) {
return res.status(401).json({
status: 401,
message: 'authorization denied',
})
}
try {
const decodedToken = await fbadmin.auth().verifyIdToken(idToken)
req.user = decodedToken as DecodedIdToken
return next()
} catch (err) {
console.log(err.message)
return res.status(401).json({
status: 401,
message: 'authorization denied',
})
}
}
The Firebase SDK does that for you. Whenever you call user.getIdToken() it will return a valid token for sure. If the existing token has expired, it will refresh and return a new token.
You can use onIdTokenChanged()and which will trigger whenever a token is refreshed and store it in your state.
However, I don't see any cons in using getIdToken() method whenever you are making an API request to server. You won't have to deal with IdToken observer and get valid token always.
const makeAPIRequest = async () => {
// get token before making API request
const token = await user.getIdToken()
// pass the token in request headers
}
Right now your code makes a request to server to get user's information whenever a token refreshes and that may be redundant.
I am trying to use Firebase Auth in backend, but I can't seem to be able to have the same Auth instance in the front-end as well.
The back-end:
'use strict';
import { firebaseAdmin, auth } from '../firebase.js';
import deleteCollection from '../helpers/deleteCollection.js';
import User from '../models/user.js';
import {
createUserWithEmailAndPassword,
updateProfile,
signInWithEmailAndPassword,
signOut,
setPersistence,
browserLocalPersistence,
} from 'firebase/auth';
const firestore = firebaseAdmin.firestore();
const register = async (req, res, next) => {
try {
// name, email, password
const { name, email, password, avatar } = req.body;
console.log('sent from frontend', { name, email, password });
// Check if email or password were sent
if (!email || !password) {
return res.status(422).json({
email: 'Email is required !',
password: 'Password is required !',
});
}
const usersCollection = firestore.collection('users');
// Reference to a QuerySnapshot whith all users that have the requested name
const userSnapshot = await usersCollection.where('name', '==', name).get();
// Check if user already exists:
if (!userSnapshot.empty) {
throw new Error('Username is taken !');
} else {
await setPersistence(auth, browserLocalPersistence);
// Firebase Auth Create User
await createUserWithEmailAndPassword(auth, email, password);
// User is signed in
const user = auth.currentUser;
if (user) {
await updateProfile(user, {
displayName: name,
});
const setUser = {
id: user.uid,
name: user.displayName,
avatar: avatar,
};
await usersCollection.doc(setUser.id).set(setUser);
res.status(201).send(setUser);
} else {
throw new Error('No user');
}
}
} catch (error) {
const errorCode = error.code;
const errorMessage = error.message;
res.status(400).send(errorMessage);
console.log(errorCode, errorMessage);
}
};
const login = async (req, res, next) => {
try {
const { email, password } = req.body;
await setPersistence(auth, browserLocalPersistence);
const userCred = await signInWithEmailAndPassword(auth, email, password);
const usersCollection = firestore.collection('users');
const userSnapshot = await usersCollection
.where('name', '==', userCred.user.displayName)
.get();
if (userSnapshot.empty) {
throw new Error('User does not exist !');
} else {
let user;
userSnapshot.forEach((doc) => (user = { ...doc.data() }));
res.status(200).send(user);
}
} catch (error) {
res.status(404).send(error.message);
console.log(error);
}
};
const logout = async (req, res, next) => {
try {
// const { name, email, password, avatar } = req.body;
await signOut(auth);
res.sendStatus(200);
} catch (error) {
const errorCode = error.code;
const errorMessage = error.message;
res.status(404).send(errorMessage);
console.log(error);
}
};
I call Register, Login and Logout using Redux thunkAPI:
const register = async (userData) => {
const response = await axios.post(API_REGISTER, userData, {
headers: {
// Overwrite Axios's automatically set Content-Type
'Content-Type': 'application/json',
},
});
if (response.data) {
// localStorage.setItem('user', JSON.stringify(response.data));
}
return response.data;
};
const login = async (userData) => {
const response = await axios.post(API_LOGIN, userData, {
headers: {
// Overwrite Axios's automatically set Content-Type
'Content-Type': 'application/json',
},
});
if (response.data) {
// localStorage.setItem('user', JSON.stringify(response.data));
}
return response.data;
};
const logout = async () => {
const response = await axios.get(`${API_LOGOUT}`);
if (response.data) {
localStorage.removeItem('user');
}
return response.data;
};
export const register = createAsyncThunk(
'user/register',
async (user, thunkAPI) => {
try {
return await userService.register(user);
} catch (error) {
return thunkAPI.rejectWithValue(error.response.data);
}
}
);
export const login = createAsyncThunk('user/login', async (user, thunkAPI) => {
try {
return await userService.login(user);
} catch (error) {
return thunkAPI.rejectWithValue(error.response.data);
}
});
export const logout = createAsyncThunk('user/logout', async (_, thunkAPI) => {
try {
return await userService.logout();
} catch (error) {
return thunkAPI.rejectWithValue(error.response.data);
}
});
I am able to Register a user, to login and to logout, but if I hit refresh I get logged out.
I am not able to persist the Firebase Auth state between front-end and backend.
This is the Private Route component
import { useSelector } from 'react-redux';
import { Navigate, useLocation } from 'react-router-dom';
import { auth } from '../../firebase';
import { useAuthState } from 'react-firebase-hooks/auth';
import { useEffect } from 'react';
import { useState } from 'react';
let isAuth;
export default function PrivateRoute({ children }) {
const location = useLocation();
const [user, setUser] = useState();
// const isAuth = useSelector((state) => state.user.user);
// const [user, loading, error] = useAuthState(auth);
// useEffect(() => {
// if (loading) return;
// if (user) {
// isAuth = true;
// console.log(user);
// }
// }, [user, loading]);
useEffect(() => {
auth.onAuthStateChanged(setUser);
}, []);
return user ? (
children
) : (
<Navigate
replace={true}
to='/login'
state={{ from: `${location.pathname}${location.search}` }}
/>
);
}
As you can see from the commented code, I've tried multiple things before posting here but nothing works.
I don't want to move the Authentication logic from back-end to front-end.
I only want to have access to the same Auth state between back-end to front-end.
The approach you're using is not supported by Firebase. You're supposed to authenticate the user only on the frontend and never on the backend. The frontend SDK will persist a token that identifies the user. You then pass that token to the backend on each call and use it to verify the user so that the backend can decide if the operation they are trying to perform is allowed. This scheme is described in the documentation, and I strongly suggest reviewing that:
If your Firebase client app communicates with a custom backend server, you might need to identify the currently signed-in user on that server. To do so securely, after a successful sign-in, send the user's ID token to your server using HTTPS. Then, on the server, verify the integrity and authenticity of the ID token and retrieve the uid from it. You can use the uid transmitted in this way to securely identify the currently signed-in user on your server.
Again, don't try to sign the user in on your backend using the frontend SDK - that is not supported and it does not scale. Only use the Firebase Admin SDK on the backend to validate the user ID tokens passed from the frontend.
I'm getting the error JsonWebTokenError: jwt must be a string when getting the jwt from the front end (react.js) and using in middleware to verify the token. If I tried to use toString it gives me another error JsonWebTokenError: jwt malformed.
Update
As soon as i pass the accessToken from frontEnd it converted into object in the AuthMiddleware.js. I'm passing middleware on header in file Post.js(attached below)
AuthMiddleware.js
const { verify } = require("jsonwebtoken")
const validateToken = (res, req, next) => {
const accesToken = req.header("accesToken");
const stringAccesToken = accesToken.toString()
console.log(typeof (stringAccesToken), "accesToken type")
if (!stringAccesToken) {
return res.json({ error: "user not logged in" })
}
try {
const validToken = verify(stringAccesToken, "importantSecret");
console.log(validToken, 'validtoken')
if (validToken) {
return next();
}
} catch (err) {
console.log(err, "error")
}
}
module.exports = { validateToken }
User.js (backend for login)
const express = require("express");
const router = express.Router()
const { Users } = require("../models")
const bcrypt = require("bcrypt")
const { sign } = require("jsonwebtoken")
router.post("/login", async (req, res) => {
const { username, password } = req.body;
const user = await Users.findOne({ where: { username: username } });
if (!user) {
return res.json({ error: "User dosen't exist" })
}
bcrypt.compare(password, user.password)
.then((match) => {
if (!match) {
return res.json({ error: "Wrong Username and Password match" })
}
const accessToken = sign({ username: user.username, id: user.id }, "importantSecret")
res.json(accessToken)
})
})
module.exports = router;
Post.js
import React, { useEffect, useState } from 'react'
import { useParams } from 'react-router-dom';
import axios from 'axios';
import './Post.css'
function Post() {
let { id } = useParams();
const [postObject, setPostObject] = useState({})
const [comments, setComments] = useState([]);
const [newComment, setNewComment] = useState("");
// console.log(comments)
const addComment = () => {
const accessToken = sessionStorage.getItem('accessToken')
console.log(typeof (accessToken), 'acces token in comment button')
axios.post(`http://localhost:4000/comments`,
{
commentBody: newComment,
PostId: id
},
{
headers: {
accessToken: sessionStorage.getItem("accessToken"),
}
}
)
.then((res) => {
// console.log(res)
const data = res.data;
console.log(data, 'comments')
setComments([...comments, data])
setNewComment("")
})
.catch((err) => {
alert(err, 'Error:comment')
})
}
useEffect(() => {
axios.get(`http://localhost:4000/posts/byId/${id}`)
.then((res) => {
// console.log(res)
const data = res.data;
// console.log(data)
setPostObject(data)
// setPost(data)
})
// comment api request
axios.get(`http://localhost:4000/comments/${id}`)
.then((res) => {
// console.log(res)
const data = res.data;
// console.log(data)
setComments(data)
})
}, [])
return (
<div className='Post'>
<div className='left__side'>
<div className='left__side__wrapper'>
<div className='title'>{postObject.title}</div>
<div className='text'>{postObject.postText}</div>
<div className='username'>{postObject.username}</div>
</div>
</div>
<div className='right__side'>
<div className='right__side__wrapper'>
<div className='add__comment__container'>
<input type="text"
value={newComment}
placeholder="Comment"
// autoComplete="off"
onChange={(e) => setNewComment(e.target.value)}
/>
<button onClick={addComment}> Submit Comment</button>
</div>
<div className='listOfCommnets'>
{comments.map((item, index) => {
{/* console.log(item, 'item') */ }
return <div className='comments' key={index}>Comments:<br />{item.commentBody}</div>
})}
</div>
</div>
</div>
</div>
)
}
export default Post
Because the jwt token is using an object as an input rather than using the word "verify," it won't work with the object in which you are receiving the error any longer. Instead, you must attempt as follows.
var jwt = require("jsonwebtoken");
const validateToken = (res, req, next) => {
const accesToken = req.header("accesToken");
const stringAccesToken = accesToken;
if (!stringAccesToken) {
return res.json({ error: "user not logged in" });
}
jwt.verify(stringAccesToken, "importantSecret", function (err, decoded) {
if (err)
return console.log(err)
// Next Code
next();
});
};
module.exports = { validateToken };
I found the solution:
As the error said JWT need to be string,
I first tried using accesToken.toString() that gives [object object],
On second try i used JSON.stringy and that was also unsuccessful.
Final scuccesful attemp was to use library name - flatted (to convert json to string and after using it i just used split till i did'nt get the token).
faltted (link) - https://github.com/WebReflection/flatted#flatted
Worked Solution -
AuthMiddleware.js
const { parse, stringify, toJSON, fromJSON } = require('flatted');
const validateToken = (res, req, next) => {
const authToken = req.header("accessToken");
const string = stringify(authToken)
const token = string && string.split(' ')[2];
const tokenPure = token.split('"')[4]
if (!tokenPure) {
return res.json({ error: "user not logged in" })
}
try {
const validToken = verify(tokenPure, "importantSecret");
// console.log(validToken, 'validtoken')
if (validToken) {
return next();
}
} catch (err) {
console.log(err, "error")
}
}
module.exports = { validateToken }
I am using firebase for authentication in my Next.js app and also I have an express server that serves a REST API, which has a middleware that uses firebase-admin to verify idToken that is sent from my app, to pass the authenticated routes
Currently
The idToken generated by firebase lasts for one hour and if the client is still on my app and
hits any route that needs idToken and if the idToken is expired then the server just throws an error as unauthenticated, which is pretty good work, but this is not desired, I know my user is in there and just idToken is expired
Question
How do I refresh my idToken of a user if it has expired, without having to do a full refresh in the browser to get new idToken
Some Code
AuthContext.tsx
/* eslint-disable no-unused-vars */
import { useRouter } from 'next/router'
import nookies from 'nookies'
import { createContext, useContext, useEffect, useState } from 'react'
import { axios } from '../config/axios'
import firebase from '../config/firebase'
import { AuthUser } from '../types'
import { BaseUser } from '../types/user'
import { getProvider } from '../utils/oAuthProviders'
type AuthContextType = {
user: AuthUser | null
login: (email: string, password: string) => Promise<any>
signup: (email: string, password: string) => Promise<any>
logout: () => Promise<any>
oAuthLogin: (provider: string) => Promise<any>
}
const AuthContext = createContext<AuthContextType>({} as AuthContextType)
export const useAuth = () => useContext(AuthContext)
const fromPaths = ['/login', '/signup']
const formatUser = (user: BaseUser, idToken: string): AuthUser => {
return {
...user,
idToken,
}
}
export const AuthContextProvider = ({ children }: { children: React.ReactNode }) => {
const [user, setUser] = useState<AuthUser | null>(null)
const [loading, setLoading] = useState(true)
const router = useRouter()
console.log(user)
useEffect(() => {
const unsub = firebase.auth().onIdTokenChanged((user) => {
if (user) {
user
.getIdToken()
.then(async (idToken) => {
try {
const userResp = await axios.get('/user/me', {
headers: {
Authorization: `Bearer ${idToken}`,
},
})
nookies.set(undefined, 'idk', idToken, { path: '/' })
const {
data: { userFullDetials },
} = userResp
setUser(formatUser(userFullDetials, idToken))
setLoading(false)
if (fromPaths.includes(router.pathname)) {
router.push('/home')
}
} catch (err) {
console.log(err)
setUser(null)
setLoading(false)
}
})
.catch((err) => {
console.log(err.message)
setUser(null)
setLoading(false)
})
} else {
setLoading(false)
setUser(null)
}
})
return () => unsub()
}, [router])
const login = (email: string, password: string) => {
return firebase.auth().signInWithEmailAndPassword(email, password)
}
const signup = (email: string, password: string) => {
return firebase.auth().createUserWithEmailAndPassword(email, password)
}
const oAuthLogin = (provider: string) => {
return firebase.auth().signInWithPopup(getProvider(provider))
}
const logout = async () => {
setUser(null)
await firebase.auth().signOut()
}
const returnObj = {
user,
login,
signup,
logout,
oAuthLogin,
}
return (
<AuthContext.Provider value={returnObj}>
{loading ? (
<div className="flex items-center justify-center w-full h-screen bg-gray-100">
<h1 className="text-indigo-600 text-8xl">S2Media</h1>
</div>
) : (
children
)}
</AuthContext.Provider>
)
}
// auth.ts
// Auth Middleware in express
import { NextFunction, Request, Response } from 'express'
import fbadmin from 'firebase-admin'
import { DecodedIdToken } from '../types/index'
export default async (req: Request, res: Response, next: NextFunction) => {
const authorization = req.header('Authorization')
if (!authorization || !authorization.startsWith('Bearer')) {
return res.status(401).json({
status: 401,
message: 'authorization denied',
})
}
const idToken = authorization.split(' ')[1]
if (!idToken) {
return res.status(401).json({
status: 401,
message: 'authorization denied',
})
}
try {
const decodedToken = await fbadmin.auth().verifyIdToken(idToken)
req.user = decodedToken as DecodedIdToken
return next()
} catch (err) {
console.log(err.message)
return res.status(401).json({
status: 401,
message: 'authorization denied',
})
}
}
The Firebase SDK does that for you. Whenever you call user.getIdToken() it will return a valid token for sure. If the existing token has expired, it will refresh and return a new token.
You can use onIdTokenChanged()and which will trigger whenever a token is refreshed and store it in your state.
However, I don't see any cons in using getIdToken() method whenever you are making an API request to server. You won't have to deal with IdToken observer and get valid token always.
const makeAPIRequest = async () => {
// get token before making API request
const token = await user.getIdToken()
// pass the token in request headers
}
Right now your code makes a request to server to get user's information whenever a token refreshes and that may be redundant.
So whenever the app is loaded it should check for user Auth using the loadUser(), the problem I'm having is that if there is no token in localStorage, the server won't return any errors(when its suppose to). I looked at the code for auth(backend), and it returns a status meassage when no token received, I was wondering is it because no token isn't a type of error, that's way the server didn't send an error response?
Below are the code snippets:
auth.js(backend)
const jwt = require("jsonwebtoken");
const config = require("config");
module.exports = function (req, res, next) {
//get token from header
const token = req.header("x-auth-token");
// check if not token
if (!token) {
return res.status(401).json({ msg: "no token, auth denied" });
}
//verify token
try {
const decoded = jwt.verify(token, config.get("jwtSecret"));
req.user = decoded.user;
next();
} catch (err) {
res.status(401).json({
msg: "token isnt valid",
});
}
};
App.js
const App = () => {
useEffect(() => {
if (localStorage.token) {
setAuthToken(localStorage.token);
store.dispatch(loadUser());
}
}, []);
auth.js Redux
export const loadUser = () => async (dispatch) => {
console.log("from auth.js");
if (localStorage.token) {
setAuthToken(localStorage.token);
}
try {
const res = await axios.get("/api/auth");
console.log("inside auth.js get auth route");
dispatch({
type: USER_LOADED,
payload: res.data,
});
} catch (err) {
console.log("error from auth.js");
dispatch({
type: AUTH_ERROR,
});
}
};
Basically the code inside catch(err) { //code }
is not executed.
Silly of me, added else condition into App.js solved the issue.