I am trying to pass res from my context into a resolver so that I can call context.res.cookie in my signin function and then send an http only cookie. I have the following code which I am not seeing the cookie added on the client but the sign in function is working besides that:
const resolvers = {
Mutation: {
signin: async (_, { email, password }, context) => {
const user = await User.findOne({ email: email });
if (!user) {
throw new Error("No such user found");
}
const valid = bcrypt.compare(password, user.password);
if (!valid) {
throw new Error("Invalid password");
}
const token = jwt.sign({ userId: user.id }, process.env.JWT_SECRET,
{
expiresIn: "30m",
});
context.res.cookie("token", token, {
httpOnly: true,
secure: true,
maxAge: 8600,
});
return {
token,
user,
};
},
},
};
I have shortened the above code but originally I am returning the JWT token and mongodb user, I am trying to also add the http cookie of the same token (it will be a different token later when I sepearte access and refresh token).
const server = new ApolloServer({
typeDefs,
resolvers,
context: async ({ req, res }) => {
/* Authentication boiler plate */
return { isAuthenticated, res };
},
});
The above code is just how I am passing the res, not sure if its needed but just in case.
The following is how the function will be called from the front end:
export const Login = () => {
const SIGN_IN = gql`
mutation Signin($email: String!, $password: String!) {
signin(email: $email, password: $password) {
token
user {
id
name
email
}
}
}
`;
const [signIn, { error, loading, data }] = useMutation(SIGN_IN);
const signInFunction = async () => {
signIn({
variables: {
email: email,
password: password,
},
});
};
if (data) {
return <Navigate to="/" />
}
};
So I needed to slightly change both my client and my server to solve my issue. On the client in apollo-client I needed to change my apolloClient from this:
const apolloClient = new ApolloClient({
uri: "http://localhost:3001/graphql",
cache: new InMemoryCache(),
});
to this:
const apolloClient = new ApolloClient({
uri: "http://localhost:3001/graphql",
cache: new InMemoryCache(),
credentials: "include",
});
Now on the server I needed to add cors like this:
const server = new ApolloServer({
typeDefs,
resolvers,
cors: {
origin: "http://localhost:3000",
credentials: true,
},
context: async ({ req, res }) => {
/* insert any boilerplate context code here */
return { isAuthenticated, res };
},
});
Thus passing res to the resolver this way works perfectly fine. However when I was getting the cookie from server now it would get deleted if I refreshed the page thus I needed an explicit expiration date, thus I changed my cookie from:
context.res.cookie("token", token, {
httpOnly: true,
secure: true,
maxAge: 8600,
});
to (24 hour expiration):
context.res.cookie("token", token, {
httpOnly: true,
secure: true,
expires: new Date(Date.now() + 24 * 60 * 60 * 1000),
});
Some notes on this solution: On the client when you add the credentials: "include", you NEED to also add the cors on the backend otherwise nothing will work, however if you remove both they will communicate fine just without cookies. Also if you add the cors and not the include nothing will break but you will not receive the cookies.
Finally this post helped me find the solution, however I did not need to setup express middleware or use apollo-link-http library as you can see above in my solution, however the post may still be helpful.
Related
I am using next js. I am using next-auth for authentication. it is a MERN stack project.
Question: How can I get jwt token and send it to my backend using next-auth and axios.
(session.jwt) is giving me undefined.
here is my nextauth.js file:
import NextAuth from 'next-auth';
import GoogleProvider from 'next-auth/providers/google';
import { MongoDBAdapter } from '#next-auth/mongodb-adapter';
import clientPromise from '../../../lib/mongodb';
export default NextAuth({
providers: [
GoogleProvider({
clientId: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
}),
],
callbacks: {
session: async ({ session, user }) => {
if (session?.user) {
session.user.id = user.id;
}
return session;
},
},
adapter: MongoDBAdapter(clientPromise),
secret: process.env.JWT_SECRET,
session: {
jwt: true,
maxAge: 30 * 24 * 60 * 60, // the session will last 30 days
},
});
The example in their docs persists the provider (account) access token in the jwt callback and then includes it to the session object:
callbacks: {
async jwt({ token, account }) {
if (account) {
token.accessToken = account.access_token;
}
return token;
},
async session({ session, token, user }) {
if (session?.user) {
session.user.id = user.id;
}
return {
...session,
accessToken: token.accessToken
};
},
}
However, if you want the raw JWT (not the provider access token) and you're server side you can use the getToken function:
export async function getServerSideProps(context) {
const token = await getToken({ req: context.req, raw: true });
// ...
}
Then to use it in your requests, here in the authorization header for example):
const config = {
headers : {
'Authorization' : `Bearer ${token}`
}
}
axios.get('http://web.com/api', config);
I'm using zendesk OAuth for authorization. I'm using the MERN stack and the current implementation works like this,
User clicks login and redirected to zendesk
once the user signs I get redirected back to /callback path
Where I sent another request to get an auth token
After I get the token I redirect the user to frontend as ?token=XXXX attached to the URL
Is this the correct way? How should I proceed with the token should I keep it in session storage? It's not a good idea to expose the token?
export const authCallback = (req: Request, res: Response): void => {
const body = {
grant_type: 'authorization_code',
code: req.query.code,
client_id: process.env.ZENDESK_CLIENT_ID,
client_secret: process.env.ZENDESK_SECRET,
}
axios
.post(`https://${process.env.SUBDOMAIN}.zendesk.com/oauth/tokens`, body, {
headers: {
'Content-Type': 'application/json',
}
})
.then((response) => {
const token = response.data.access_token
return res.redirect(`${process.env.ORIGIN}?token=${token}`)
})
.catch((err) => {
return res.status(400).send({ message: err.message })
})
}
Either use express-session and store the token on the server in req.session.token:
(response) => {
req.session.token = response.data.access_token;
req.session.save(function() {
res.redirect(`${process.env.ORIGIN}`)
});
}
Or send the token in a session cookie directly:
(response) => {
res.cookie("token", response.data.access_token, {
httpOnly: true,
secure: true,
sameSite: "None"
});
res.redirect(`${process.env.ORIGIN}`)
}
I am working on user authentication for a website built using the MERN stack and I have decided to use JWT tokens stored as HttpOnly cookies. The cookie was sent in a "Set-Cookie" field in response header when I used Postman to make the request but not in the Safari Web Inspector as shown in the image below. There are no cookies found in the storage tab either.
I have simplified my React login form to a button that submits the username and password of the user for the sake of debugging
import React from "react";
const sendRequest = async (event) => {
event.preventDefault();
let response;
try {
response = await fetch("http://localhost:5000/api/user/login", {
method: "POST",
body: { username: "Joshua", password: "qwerty" },
mode: "cors",
// include cookies/ authorization headers
credentials: "include",
});
} catch (err) {
console.log(err);
}
if (response) {
const responseData = await response.json();
console.log(responseData);
}
};
const test = () => {
return (
<div>
<input type="button" onClick={sendRequest} value="send" />
</div>
);
};
export default test;
I am using express on the backend and this is my index.js where all incoming requests are first received
const app = express();
app.use(bodyParser.json());
app.use("/images", express.static("images"));
app.use((req, res, next) => {
res.set({
"Access-Control-Allow-Origin": req.headers.origin,
"Access-Control-Allow-Credentials": "true",
"Access-Control-Allow-Headers": "Content-Type, *",
"Access-Control-Allow-Methods": "GET, POST, PATCH, DELETE",
});
next();
});
app.use(cookieParser());
// api requests for user info/ login/signup
app.use("/api/user", userRoutes);
This is the middleware that the login request is eventually directed to
const login = async (req, res, next) => {
const { username, password } = req.body;
let existingUser;
let validCredentials;
let userID;
let accessToken;
try {
existingUser = await User.findOne({ username });
} catch (err) {
return next(new DatabaseError(err.message));
}
// if user cannot be found -> username is wrong
if (!existingUser) {
validCredentials = false;
} else {
let isValidPassword = false;
try {
isValidPassword = await bcrypt.compare(password, existingUser.password);
} catch (err) {
return next(new DatabaseError(err.message));
}
// if password is wrong
if (!isValidPassword) {
validCredentials = false;
} else {
try {
await existingUser.save();
} catch (err) {
return next(new DatabaseError(err.message));
}
userID = existingUser.id;
validCredentials = true;
accessToken = jwt.sign({ userID }, SECRET_JWT_HASH);
res.cookie("access_token", accessToken, {
maxAge: 3600,
httpOnly: true,
});
}
}
res.json({ validCredentials });
};
Extra information
In the login middleware, a validCredentials boolean is set and returned to the client. I was able to retrieve this value on the front end hence I do not think it is a CORS error. Furthermore, no errors were thrown and all other API requests on my web page that do not involve cookies work fine as well.
Another interesting thing is that despite using the same data (A JS object containing {username:"Joshua", password:"qwerty"}) for both Postman and the React code, validCredentials evaluates to true in Postman and false in the Web Inspector. It is an existing document in my database and I would expect the value returned to be true, which was the case before I added cookies
May I know what I have done wrong or do you have any suggestions on how I can resolve this issue? I am a beginner at web-development
EDIT
With dave's answer I can receive the "Set-Cookie" header on the frontend. However it does not appear in the Storage tab in the web inspector for some reason.
This is the response header
This is the Storage tab where cookies from the site usually appears
If you're trying to send the request as json, you need to set the content type header, and JSON.stringify the object:
response = await fetch("http://localhost:5000/api/user/login", {
method: "POST",
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username: "Joshua", password: "qwerty" }),
mode: "cors",
// include cookies/ authorization headers
credentials: "include",
});
Right now you're probably getting the equivalent of
existingUser = User.findOne({ username: undefined})
and so when you do:
if (!existingUser) {
validCredentials = false;
} else { /* ... */ }
you get the validCredentials = false block, and the cookie is set in the other block.
You can not see it because you have made it httpOnly cookie.
So the first time running this code, the browser set the cookie. But then when I cleared the cookie from the browser and tried to run the code again, the browser isn't setting the cookie any more. I tried multiple browsers and it doesn't work. The fetch request is successful and i can print the cookie value in the console, but the browser wont set the cookie. This is a very annoying problem. I tried using the credential: 'include' also but it didnt work. Getting a CORS error.
I'm pretty new to web development so my knowledge is not very deep.
This is the code on my front end
let reqObj = {
//check email to see if it is a valid format in the login.html user email input
//name: name.value,
email: email.value,
password: password.value
}
const response = await fetch(api_url + "api/user/login", {
method: 'POST',
body: JSON.stringify(reqObj),
headers: {
'Content-Type': 'application/json'
},
credentials: 'same-origin'
});
const jwt = await response.text();
console.log(jwt)
this is the express server code
router.post('/login', async (req, res) => {
//validate data
const { error } = loginValidation(req.body)
if (error) return res.status(400).send(error.details[0].message);
//checking if email is in database
const user = await User.findOne({ email: req.body.email });
if (!user) return res.status(400).send('Email or password is incorrect')
//Password is correct
const validPass = await bcrypt.compare(req.body.password, user.password)
if (!validPass) return res.status(400).send('Invalid password')
// Create and assign a token
const token = jsonWebToken.sign({ _id: user.id }, process.env.TOKEN_SECRET);
//res.header('auth-token', token).send(token);
res.cookie('auth_token', token, {
maxAge: 3600,
httpOnly: true
}).send(token)
});
module.exports = router;
I believe that in order to use credentials: 'include' you have to add the Access-Control-Allow-Credentials header on your response.
is there an easy way to disable SSL validation in Axios. I tried this process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; but it doesn't work.
Here's an example of my code"
const postPosts = () => {
axios
.post("https://xxx.dev.lab", {
Username: "xxx",
Password: "xxx"
})
.then(response => {
console.log(response);
})
.catch(error => {
console.error(error);
});
};
postPosts();
Axios doesn't address that situation so far - you can try:
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
BUT THATS A VERY BAD IDEA since it disables SSL across the whole node server..
or you can configure axios to use a custom agent and set rejectUnauthorized to false for that agent as mentioned here
example:
// At instance level
const instance = axios.create({
httpsAgent: new https.Agent({
rejectUnauthorized: false
})
});
instance.get('https://something.com/foo');
// At request level
const agent = new https.Agent({
rejectUnauthorized: false
});
axios.get('https://something.com/foo', { httpsAgent: agent });
[ IF YOU RUNNING YOUR APP IN THE DOCKER ]
I solved that issue in my project with 2 steps:
1. Change Docker SSL settings
I edited /etc/ssl/openssl.cnf inside the container
Replace strings:
TLSv1.2 => TLSv1
SECLEVEL=2 => SECLEVEL=1
2. Set min TLS version for your request
import * as https from 'https';
const agent = new https.Agent({
rejectUnauthorized: false,
minVersion: 'TLSv1',
});
axios.post(
"https://xxx.dev.lab",
{ Username: "xxx", Password: "xxx" },
{ httpsAgent: agent }
)