Authentication failure after page reload (Vue, Expressjs with Nodejs) - node.js

Current situation
I am developing nodejs backend server and vue frontend application, which is run under different port(localhost:3000 and localhost:8080). With purpose to enable CORS connection, I configured devServer proxy from vue.config.js file.
vue.config.js
module.exports = {
devServer: {
proxy: {
'/users': {
target: 'http://127.0.0.1:3000/users',
changeOrigin: true,
pathRewrite: {
'^/users':''
}
},
'/tasks': {
target: 'http://127.0.0.1:3000/tasks',
changeOrigin: true,
pathRewrite: {
'^/tasks': ''
}
}
}
},
outputDir: '../backend/public'
}
and technically used cors.js to enable request to backend server, which was implemented by expressjs.
I am sending the request with vue component to retrieve data from backend server. It works properly from fetching data from server, and my goal is to make the same behavior when I reload page. However, whenever I reload same page, it keep showing 401 http response status set by the backend code written by myself.
enter image description here
Research and Trial til now
Before I go on the attempts I have tried, I should introduce mandatory codes to be operated at first. Somehow this is at least explanations in which vuex actions using axios, axios using backend routers eventually.
tasks.module.js
import axios from "axios"
import authHeader from '../../services/auth-header'
export const tasks = {
state: {
tasks: []
},
getters: {
allTasks: (state) => state.tasks
},
actions: {
async fetchTasks({ commit }) {
const response = await axios.get('http://127.0.0.1:3000/tasks', {headers: authHeader()})
commit('setTasks', response.data)
axios.defaults.headers.common['Authorization'] = authHeader()
},
async addTask({ commit }, description) {
const response = await axios.post('http://127.0.0.1:3000/tasks', { description, completed: false}, {headers: authHeader()})
commit('newTask', response.data)
},
async updateTask({ commit }, updTask) {
const response = await axios.patch('http://127.0.0.1:3000/tasks/'+updTask.id, updTask, {headers: authHeader()})
commit('updateTask', response.data)
}
},
mutations: {
setTasks: (state, tasks) => (state.tasks = tasks),
newTask: (state, task) => state.tasks.unshift(task),
updateTask: (state, updTask) => {
let updates = Object.keys(updTask)
updates.forEach((update) => {
state.task[update] = updTask[update]
})
}
}
}
TaskManager.vue
<template>
<div class="container">
<div class="jumbotron">
<h3>Task Manager</h3>
<AddTask/>
<Tasks/>
</div>
</div>
</template>
<script>
import Tasks from './components/Tasks'
import AddTask from './components/AddTask'
export default {
name:'TaskManager',
components: {
Tasks,
AddTask
}
}
</script>
Tasks.vue
<template>
<div>
<div>
<div class="legend">
<span>Double click to mark as complete</span>
<span>
<span class="incomplete-box"></span> = Incomplete
</span>
<span>
<span class="complete-box"></span> = Complete
</span>
</div>
</div>
<div class="tasks">
<div
#dblclick="onDblClick(task)"
v-for="task in allTasks"
:key="task.id"
class="task"
v-bind:class="{'is-completed':task.completed}">
{{task.description}}
</div>
</div>
</div>
</template>
<script>
import { mapGetters, mapActions } from 'vuex'
export default {
name: "Tasks",
methods:{
...mapActions(['fetchTasks', 'updateTask']),
onDblClick(task) {
const updTask = {
id: task._id,
description: task.description,
completed: !task.completed
}
console.log(updTask)
this.updateTask(updTask)
}
},
computed: {
...mapGetters(['allTasks']),
},
created() {
this.fetchTasks()
}
}
Now I need to introduce what I have tried to solve problems
Configuring CORS options
Since this error page didnt show any authorization header which was supposed to set in request header I figured out the way I enabled cors connection and I believe this enables preflight request. Here is what I configured middleware behavior from backend code.
task.js(express router file)
const router = new express.Router()
const auth = require('../middleware/auth')
const cors = require('cors')
const corsOptions = {
origin: 'http://127.0.0.1:3000',
allowedHeaders: 'content-Type, Authorization',
maxAge:3166950
}
router.options(cors(corsOptions))
router.get('/tasks', auth, async (req, res) => {
const match = {}
const sort = {}
if(req.query.completed) {
match.completed = req.query.completed === 'true'
}
if(req.query.sortBy) {
const parts = req.query.sortBy.split('_')
sort[parts[0]] = parts[1] === 'desc' ? -1:1 // bracket notation
}
try {
await req.user.populate({
path: 'tasks',
match,
options: {
limit: parseInt(req.query.limit),
skip: parseInt(req.query.skip),
sort
}
}).execPopulate()
console.log(req.user.tasks)
res.status(200).send(req.user.tasks)
} catch (e) {
res.status(500).send(e)
}
})
module.exports = router
auth.js(middleware)
const jwt = require('jsonwebtoken')
const User = require('../models/user')
const auth = async (req, res, next) => {
try {
const token = req.header('Authorization').replace('Bearer ','')
const decoded = jwt.verify(token, 'thisisnewcourse')
console.log('decoded token passed')
const user = await User.findOne({ _id: decoded._id, 'tokens.token': token})
console.log('user found')
if(!user) {
throw new Error()
}
req.token = token
req.user = user
next()
} catch (error) {
console.log('error caught')
res.status(401).send({error: 'please authenticate'})
}
}
module.exports = auth
Set Authorization header as axios default header after login
auth.module.js(since login works correctly, I am copying only login action part)
actions: {
async login ({ commit }, user){
try {
const response = await axios.post('http://127.0.0.1:3000/users/login', user)
if(response.data.token){
localStorage.setItem('user', JSON.stringify(response.data))
axios.defaults.headers.common['Authorization'] = `Bearer ${response.data.token}`
}
commit('loginSuccess', response.data)
} catch (e) {
console.log(e)
}
}
Middleware chaining on the express route(cors, auth)
I have tried two different middleware on the same backend code(task.js)
router.get('/tasks', [cors(corsOptions), auth], async (req, res) => {
// same as previously attached code
}
Now I believe referring to another post with similar issue will help me out however it's about having CORS enabled, not the issue that the header is not sent via either preflight request or other type of requests.

You haven't included the code for authHeader but I assume it just returns the value of the Authorization header.
This bit looks suspicious to me:
async fetchTasks({ commit }) {
const response = await axios.get('http://127.0.0.1:3000/tasks', {headers: authHeader()})
commit('setTasks', response.data)
axios.defaults.headers.common['Authorization'] = authHeader()
},
The final line seems to be trying to set the Authorization header globally so that it will be included on all subsequent axios requests. That's fine but it seems strange not to do that sooner. You have a similar line inside the login action, which makes sense, but I assume that isn't being called when the page is refreshed.
Then there's this bit:
{headers: authHeader()}
If authHeader returns the value of the Authorization header then this won't work. Instead you need:
{headers: { Authorization: authHeader() }}
Ideally you wouldn't need to set any headers here and instead you'd just set the global header before attempting this request.
While it isn't the direct cause of your problem, you seem to have got your wires crossed about CORS. You've configured a proxy, which means you aren't using CORS. The request you're making is to the same origin, so CORS doesn't apply. You don't need to include CORS response headers if you aren't making a cross-origin request. If you do want to make a cross-origin request then don't use the proxy. You should try to mimic your production environment during development, so if you intend to use CORS in production you should use it during development. Otherwise, stick with the proxy.

Related

Calling my Next JS /api endpoint from getStaticProps will not set a cookie

I am getting data from a remote API. The remote API uses a JS Web Token to allow access. In my getStaticProps function, I call my local /api endpoint and in that endpoint I check to see if there is a cookie already set, containing the last good token. If not, I call the remote API for a fresh token and set a new cookie with that token.
This is to save the remote API being called every time I need to get data from that server. All of my tests worked perfectly (when I directly called the /api endpoint using my browser), with my /api endpoint getting the remote token only when needed. However, when I wired the solution into my app and placed the local /api call in getStaticProps, the /api stopped setting the cookie altogether and kept calling the remote API for the token. I have stripped out my code to create a test which gives the same behaviour. Sample below:
/cookietest.js
function cookietest({tokenObj}) {
const pageTitle = 'cookie test'
return (
<div>
<Head>
<title>Cookie tester</title>
</Head>
<Header pageTitle={pageTitle} />
<p>{tokenObj.token}</p>
<p>{tokenObj.message}</p>
</div>
)
}
export const getStaticProps = async () => {
async function getToken() {
let tokenObject
await fetch('http://localhost:3000/api/otherpath/token')
.then(response => response.json())
.then((data) => {
tokenObject = data
})
return tokenObject
}
const tokenObj = await getToken()
return {
props: {
tokenObj: tokenObj
}
}
}
export default cookietest
/otherpath/token/index.js
import cookie from 'cookie'
export default async (req, res) => {
res.setHeader("Set-Cookie", cookie.serialize("token", 'adffdafadf', {
httpOnly: true,
secure: true,
maxAge: 60 * 60,
sameSite: "strict",
path: "/"
}))
const returnObject = {
token: 'kdkdkkdkdk',
message: 'Success - from local test'
}
res.statusCode = 200;
res.json(returnObject)
}
So, this returns my manual token perfectly and it gets displayed on the 'cookietest' page. But, the cookie never gets set. Can anyone tell me why this happens and give me a better method to make this work?
Cookies may only be set in the interaction between the browser and the server. Your sample code is an interaction between two servers. The set cookie never gets to the browser.
To pass the cookie to the browser you'd need your cookietest.js to look something like:
import cookie from 'cookie'
function cookietest({tokenObj}) {
const pageTitle = 'cookie test'
return (
<div>
<Head>
<title>Cookie tester</title>
</Head>
<Header pageTitle={pageTitle} />
<p>{tokenObj.token}</p>
<p>{tokenObj.message}</p>
</div>
)
}
export const getServerSideProps = async (ctx) => {
async function getToken() {
let cookieValue = null;
const tokenObject = await fetch('http://localhost:3000/api/otherpath/token')
.then(response =>{
cookieValue = response.headers.get('set-cookie').split(',').reduce((a,c)=>{
if(!a){
const nvp = (c.split(';')[0]).split('=');
if(nvp[0] == 'token') return nvp[1];
}
return a;
}, null);
return response.json();
});
return {
tokenObject,
cookieValue
};
}
const {tokenObj, cookieValue} = await getToken();
ctx.res.setHeader("Set-Cookie", cookie.serialize("token", cookieValue, {
httpOnly: true,
secure: true,
maxAge: 60 * 60,
sameSite: "strict",
path: "/"
}));
return {
props: {
tokenObj: tokenObj
}
}
}
export default cookietest

How can I logout user on route change once JWT token is expired?

I'm using Passport JWT and I want to check JWT token validity to perform a logout if it's already expired. I already made it work on page refresh but not on route change (from navbar, for example). I need to find a way to check it every time any route component is re-rendered, and I know it's very likely that I don't need to do it on every single component. I just don't know how.
Here's the code that's working for page refresh:
On Express
authenticated.js
router.route("/").get((req, res) => {
passport.authenticate("jwt", { session: false }, (err, user, info) => {
if (!user) {
res.status(401);
res.send("Unauthorized")
}
})(req, res);
});
module.exports = router;
On React
AuthContext.js
export const AuthContext = createContext();
function AuthProvider({ children }) {
const [isLoaded, setIsLoaded] = usePersistedState(false, "loaded");
const [user, setUser] = usePersistedState(null, "username");
const [isAuthenticated, setIsAuthenticated] = usePersistedState(
false,
"auth"
);
useEffect(() => {
axios
.get("/authenticated")
.then((response) => {
return;
})
.catch((error) => {
setUser("");
setIsAuthenticated(false);
setIsLoaded(true);
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<div>
{!isLoaded ? (
<h3>Loading...</h3>
) : (
<AuthContext.Provider
value={{ user, setUser, isAuthenticated, setIsAuthenticated }}
>
{children}
</AuthContext.Provider>
)}
</div>
);
}
export default AuthProvider;
index.js
ReactDOM.render(
<AuthProvider>
<App />
</AuthProvider>,
document.getElementById("root")
);
UPDATE
I tried to get it done by adding this useEffect function on my PrivateOutlet.js, which handles the private routes, but it didn't work:
function PrivateOutlet() {
const authContext = useContext(AuthContext);
const location = useLocation();
useEffect(() => {
axios
.get("/authenticated")
.then((response) => {
return;
})
.catch((err) => {
authContext.setIsAuthenticated(false);
authContext.setUser("");
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [location]);
return authContext.isAuthenticated ? (
<Outlet />
) : (
<Navigate to="/login" replace state={{ path: location.pathname }} />
);
}
export default PrivateOutlet;
The response is not specific to any platform/language. In general, the authentication provider/service is issuing the token to the client. The token meant for accessing the private resoruces upon validating by resouce server. As you're using the JWT token, so a resouce server/service can validate the token without talking to authentication service or without performing a database look-up. Your server side code always need to validate the token with the incoming requests, which you can do by creating an interceptor for protected requests. In short, you can invoke call from client to validate the token whenever required.
it could be a socket call for the checking through a wrapper component. If the socket returns false, then history.push him into a page where the login is not required
The approach listed on the "update" section from the original question was on the right path, however there was some caveats on my code preventing it to work properly.
I forgot to remove the useEffect() from AuthContext.js after moving it to PrivateOutlet.js, and I also modified the dependency array to include location.pathname instead of just location:
PrivateOutlet.js
function PrivateOutlet() {
const authContext = useContext(AuthContext);
const location = useLocation();
useEffect(() => {
axios
.get("/authenticated")
.then((response) => {
return;
})
.catch((err) => {
authContext.setIsAuthenticated(false);
authContext.setUser("");
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [location.pathname]);
return authContext.isAuthenticated ? (
<Outlet />
) : (
<Navigate to="/login" replace state={{ path: location.pathname }} />
);
}
export default PrivateOutlet;
This was a minor change and probably didn't interfere at all on anything, but in authenticated.js on my Express server I just moved back from using a custom callback to the standard Passport callback, allowing it to handle unauthorized calls by itself:
authenticated.js
router
.route("/")
.get(passport.authenticate("jwt", { session: false }), (req, res) => {
//do some stuff
res.json();
});
module.exports = router;

How to clear an set cookies with Apollo Server

I recently just switched from using express with apollo server to just using apollo server since the subscriptions setup seemed more current and easier to setup. The problem I'm having now is I was saving a cookie with our refresh token for login and clearing the cookie on logout. This worked when I was using express.
const token = context.req.cookies[process.env.REFRESH_TOKEN_NAME!];
context.res.status(401);
Since switching from express/apollo to just apollo server I don't have access to req.cookies even when i expose the req/res context on apollo server.
I ended up switching to this (which is hacky) to get the cookie.
const header = context.req.headers.cookie
var cookies = header.split(/[;] */).reduce(function(result: any, pairStr: any) {
var arr = pairStr.split('=');
if (arr.length === 2) { result[arr[0]] = arr[1]; }
return result;
}, {});
This works but now I can't figure out how to delete the cookies. With express I was doing
context.res.clearCookie(process.env.REFRESH_TOKEN_NAME!);
Not sure how I can clear cookies now since res.clearCookie doesn't exist.
You do not have to specifically clear the cookies. The expiresIn cookie key does that for you. Here is the snippet which i used to set cookies in browser from apollo-server-lambda. Once the expiresIn date values has passed the current date time then the cookies wont be valid for that host/domain. You need to revoke access token for the user again or logout the user from the application
import { ApolloServer, AuthenticationError } from "apollo-server-lambda";
import resolvers from "./src/graphql/resolvers";
import typeDefs from "./src/graphql/types";
const { initConnection } = require("./src/database/connection");
const { validateAccessToken, hasPublicEndpoint } = require("./src/bll/user-adapter");
const { addHoursToDate } = require("./src/helpers/utility");
const corsConfig = {
cors: {
origin: "http://localhost:3001",
credentials: true,
allowedHeaders: [
"Content-Type",
"Authorization"
],
},
};
// creating the server
const server = new ApolloServer({
// passing types and resolvers to the server
typeDefs,
resolvers,
context: async ({ event, context, express }) => {
const cookies = event.headers.Cookie;
const accessToken = ("; " + cookies).split(`; accessToken=`).pop().split(";")[0];
const accessLevel = ("; " + cookies).split(`; accessLevel=`).pop().split(";")[0];
const expiresIn = ("; " + cookies).split(`; expiresIn=`).pop().split(";")[0];
const { req, res } = express;
const operationName = JSON.parse(event.body).operationName;
if (await hasPublicEndpoint(operationName)) {
console.info(operationName, " Is a public endpoint");
} else {
if (accessToken) {
const jwtToken = accessToken.split(" ")[1];
try {
const verifiedUser = await validateAccessToken(jwtToken);
console.log("verifiedUser", verifiedUser);
if (verifiedUser) {
return {
userId: verifiedUser,
};
} else {
console.log();
throw new AuthenticationError("Your token does not verify!");
}
} catch (err) {
console.log("error", err);
throw new AuthenticationError("Your token does not verify!");
}
}
}
return {
headers: event.headers,
functionName: context.functionName,
event,
context,
res,
};
},
cors: corsConfig,
formatResponse: (response, requestContext) => {
if (response.data?.authenticateUser || response.data?.revokeAccessToken) {
// console.log(requestContext.context);
const { access_token, user_type, access_token_generated_on, email } =
response.data.authenticateUser || response.data.revokeAccessToken;
const expiresIn = addHoursToDate(new Date(access_token_generated_on), 12);
requestContext.context.res.set("set-cookie", [
`accessToken=Bearer ${access_token}`,
`accessLevel=${user_type}`,
`expiresIn=${new Date(access_token_generated_on)}`,
`erUser=${email}`,
]);
}
if (response.data?.logoutUser) {
console.log("Logging out user");
}
return response;
},
});
Simply send back the exact same cookie to the client with an Expires attribute set to some date in the past. Note that everything about the rest of the cookie has to be exactly the same, so be sure to keep all the original cookie attributes, too.
And, here's a link to the RFC itself on this topic:
Finally, to remove a cookie, the server returns a Set-Cookie header
with an expiration date in the past. The server will be successful
in removing the cookie only if the Path and the Domain attribute in
the Set-Cookie header match the values used when the cookie was
created.
As to how to do this, if you're using Node's http module, you can just use something like this (assuming you have a response coming from the callback passed to http.createServer):
context.response.writeHead(200, {'Set-Cookie': '<Your Cookie Here>', 'Content-Type': 'text/plain'});
This is assuming that your context has access to that http response it can write to.
For the record, you can see how Express does it here and here for clarity.

How do I solve CORS issues in Apollo, Node, and Next.js?

I need help troubleshooting I CORS error I am having in Apollo, Node, and Next.js. I am not sure what change I have made, but suddenly I am unable to fetch the data from my Prisma database. I am currently running dev mode. My Yoga server which pulls in my data from Prisma run at localhost:4444. My frontend is run on localhost:7777.
Here is my CORS setup:
import withApollo from "next-with-apollo";
import ApolloClient from "apollo-boost";
import { endpoint, prodEndpoint } from "../config";
import { LOCAL_STATE_QUERY } from "../components/Cart";
function createClient({ headers }) {
return new ApolloClient({
uri: process.env.NODE_ENV === "development" ? endpoint : prodEndpoint,
request: (operation) => {
operation.setContext({
fetchOptions: {
credentials: "include",
},
headers,
});
},
// local data
clientState: {
resolvers: {
Mutation: {
toggleCart(_, variables, { cache }) {
// read the cartOpen value from the cache
const { cartOpen } = cache.readQuery({
query: LOCAL_STATE_QUERY,
});
// Write the cart State to the opposite
const data = {
data: { cartOpen: !cartOpen },
};
cache.writeData(data);
return data;
},
},
},
defaults: {
cartOpen: false,
},
},
});
}
export default withApollo(createClient);
variables.env
FRONTEND_URL="localhost:7777"
PRISMA_ENDPOINT="https://us1.prisma.sh/tim-smith-131869/vouch4vet_dev_backend/dev"
PRISMA_SECRET="..."
APP_SECRET="..."
STRIPE_SECRET="..."
PORT=4444
backend index.js
const server = createServer();
server.express.use(cookieParser());
// decode the JWT so we can get the user Id on each request
server.express.use((req, res, next) => {
const { token } = req.cookies;
if (token) {
const { userId } = jwt.verify(token, process.env.APP_SECRET);
// put the userId onto the req for future requests to access
req.userId = userId;
}
next();
});
I have tried rolling back to previous commit and I have had no luck. I have not ruled out internet problems.
Let me know if you need to see the rest of my repo.
Thanks

How to fetch and display data from Express API to a React app?

I have recently started learning react an I am trying to create an application which makes use of an API which I created using Express.
Currently I am hosting my Express API locally at http://localhost:3000/api
However, when i open postman or my browser and send a GET request to http://localhost:3000/api/tasks I get all the tasks that are on my API, as expected.
My react application looks like this
class App extends React.Component{
constructor(){
super()
this.state = {
tasks: {}
}
}
...
componentDidMount() {
this.setState({loading: true})
fetch("http://localhost:3000/api/tasks/1")
.then(response => response.json())
.then(data => {
this.setState({
tasks: data
})
})
}
...
render() {
return(
<div>
<p>{this.state.tasks.id}</p>
</div>
}
}
When I open the dev tab in my browser I can see that the request (code: 304) is sent and there is a response which is a json file with the object I need. Nevertheless, I am unable to display the data I fetch from the API.
The problem was that the CORS was not enabled! After adding:
app.use(function(req, res, next) {
res.header("Access-Control-Allow-Origin", "*");
res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, Authorization");
res.header('Access-Control-Allow-Methods', 'PUT, POST, GET, DELETE, OPTIONS');
next();
});
to my index.js file of the Express API I was able to fetch and display the data from the API to the react application.
There is a node.js package for providing a Connect/Express middleware that can be used to enable CORS with various options.
(npm cors)
I can give you my sample code since it is hard to see error from your code without debugging.
I am using Nextjs and here is how I get data from server side by using fetch method.
const SamplePage = props => {
// you can get data from api here from props.data
const [ testState, setTestState] = useState({
test: props.data,
});
return (
...
)
}
SamplePage.getInitialProps = async ctx => {
try {
const { token, username } = nextCookie(ctx);
const apiUrl = getHost(ctx.req) + `/api/portfolio/${ctx.query.id}`;
const response = await fetch(apiUrl, {
method: 'GET',
headers: {
Authorization: JSON.stringify({ token, username })
}
});
if (response.ok) {
const data = await response.json();
return { data };
} else {
// add your action when response is not ok
}
} catch (error) {
// add your action when have error
}
};
export default withAuthSync(SamplePage);

Resources