I am having troubles understanding how to protect an API with next-auth using Google authentication, I have searched a lot online but there is not a good guide on how to achieve this, at the moment I have created the [...nextauth].js as follows:
import NextAuth from 'next-auth';
import GoogleProvider from 'next-auth/providers/google';
import { MongoDBAdapter } from "#next-auth/mongodb-adapter"
import connection from "../../../lib/database";
const options = {
secret: process.env.JWT_SECRET,
adapter: MongoDBAdapter(connection),
providers: [
GoogleProvider({
clientId: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
}),
],
pages: {
signIn: '/signin'
},
callbacks: {
async signIn({ user, account, profile, email, credentials }) {
return true;
},
async redirect({ url, baseUrl }) {
return baseUrl;
},
async session({ session, user, token }) {
return session;
},
async jwt({ token, user, account, profile, isNewUser }) {
return token;
}
}
}
export default (req, res) => NextAuth(req, res, options)
The effect I am trying to achieve is to send the accessToken to the api on each request, when the api receives the request validates the token and if the token is ok then sends the response otherwise redirects the user to the /signin page.
I believe I should add the token in the axios Bearer in the signin callback am I right? However I have no idea on how to write the middleware that checks if the jwt token is correct.
Related
I am using NextJS Request helpers to implement a proxy api.
Upon successful login I set an access_token within the getServerSideProps using cookies-next
export async function getServerSideProps(ctx) {
let login = await CompleteLogin(ctx.query.session);
if (login?.type === 200) {
deleteCookie("token", ctx);
setCookie("access_token", login.data.token, {
secure: true,
httpOnly: true,
res: ctx.res,
req: ctx.req,
expires: new Date(login.data.expiry),
path: "/",
});
}
return {
props: {
...returnProps,
},
};
}
The cookie set's no problem, I can see it being stored.
Subsequent requests then go via the proxy handler /api/[...path].ts, however within the proxy handler I am unable to get the access_token cookie.
import { getCookie } from "cookies-next";
import type { NextApiRequest, NextApiResponse } from "next";
import httpProxyMiddleware from "next-http-proxy-middleware";
export const config = {
api: {
bodyParser: false,
externalResolver: true,
},
};
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const access_token = getCookie("access_token", { req, res });
console.log("token api", access_token); // undefined
httpProxyMiddleware(req, res, {
headers: {
Authorization: `Bearer ${token}`,
},
target: process.env.API_URL,
});
};
export default handler;
Any idea where I am going wrong? I have implement this proxy style before and had no issues so I am assuming I have missed something quite obvious?
I have tried different ways of setting the cookie, custom proxy handler, but the only way I have been able to get it to work is not setting the cookie to be HTTPOnly, which defeats the need of the proxy.
The below code is code from [...nextauth].js .
The Goal is to achieve is to send POST request to save data and to set a session token with the returned result when using google-authentication.
To explain the code written: I am using next-auth's credential and google providers. In the Credential provider I am making a POST request to check for the user in the database hosted on localhost:8080. The credentials passed as parameters include email and password.
For Google Provider, I have kept the code default from the doc.
callbacks are there to save tokens.
import NextAuth from "next-auth"
import GoogleProvider from "next-auth/providers/google";
import CredentialsProvider from "next-auth/providers/credentials";
export default NextAuth({
// Configure one or more authentication providers
providers: [
CredentialsProvider({
async authorize(credentials){
//check if crenditials.email is present in database
const res =await fetch('http://localhost:8080/user/login?deviceToken=eEiLMMkzR1ypiCwp068z97:APA91bEiBpfwCmpZ5-ijVU4FKcl-4d0QkuWrBtXgcZRJF06MUw8GJvcBn_4ci-v1IFOD8wMF0bNqEheFq0LR0Vz5hXIktT-7sMwOfR52ULhy14NgjiUUW_0nNs5gBXAZHwhtifJluS7v', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(credentials),
})
const x=await res.json();
// console.log(x);
const user={email:x.user.email,name:`${x.user.firstName} ${x.user.lastName}`};
if(res.ok && user){
console.log("logged In");
return user;
}
console.log("error1");
return null;
}}),
GoogleProvider({
clientId: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
authorization: {
params: {
prompt: "consent",
access_type: "offline",
response_type: "code"
}
}
}),
],
jwt: {
encryption:true,
},
callbacks:{
async jwt(token,account)
{
console.log(account);
if(account){
token.accessToken = account.accessToken;
}
return token;
},
}
})
You could use the signIn callback, however I'm not sure if this would be the purpose of this callback, it's what I'm doing at the moment.
This function gets called after the sign in process so you have access to the user's data.
async signIn({ account, profile }) {
if (account.provider === "google") {
// we can do DB queries here
console.log({
verified: profile.email_verified,
name: profile.given_name,
email: profile.email,
lastName: profile.family_name
})
return true
}
return true // do other things for other providers
}
I can't get the token with getToken:
This variables are ok:
NEXTAUTH_SECRET=secret
NEXTAUTH_URL=http://localhost:3000
Here is my [...nextauth].js - I can do console.log(token) and it works well
import NextAuth from "next-auth";
import GoogleProvider from "next-auth/providers/google";
...
jwt: {
secret: process.env.JWT_SECRET,
encryption: true,
},
secret: process.env.NEXTAUTH_SECRET,
callbacks: {
async redirect({ url, baseUrl }) {
return Promise.resolve(url);
},
async jwt({ token, user, account, profile, isNewUser }) {
return token;
},
async session({ session, user, token }) {
return session;
},
},
});
API section (I think getToken doesnt work well):
import { getToken } from "next-auth/jwt";
const secret = process.env.NEXTAUTH_SECRET;
export default async (req, res) => {
const token = await getToken({ req, secret, encryption: true });
console.log(token);
if (token) {
// Signed in
console.log("JSON Web Token", JSON.stringify(token, null, 2));
} else {
// Not Signed in
res.status(401);
}
res.end();
};
This might be a bug in the function but the only way i got it to work was to use getToken to get the raw jwt token and then use jsonwebtoken package to verify and decode it
import { getToken } from "next-auth/jwt";
import jwt from "jsonwebtoken";
const secret = process.env.NEXT_AUTH_SECRET;
const token = await getToken({
req: req,
secret: secret,
raw: true,
});
const payload = jwt.verify(token, process.env.NEXT_AUTH_SECRET);
console.log(payload);
this worked for me:
const secret = process.env.SECRET
const token = await getToken({ req:req ,secret:secret});
also check [...nextauth].js, if you're using
adapter: MongoDBAdapter(clientPromise),
then you dont have JWT token, you have to set
session: {
strategy: "jwt",
},
notice this will save the session locally and not on the database
more info here: https://next-auth.js.org/configuration/options#session
This issue still exists, but unlike other answers, I simply had to pass the NEXTAUTH_SECRET that I had set on my .env file to my getToken() function without setting raw: true.
import { getToken } from "next-auth/jwt";
const secret = process.env.NEXTAUTH_SECRET;
export default async (req, res) => {
const token = await getToken({ req: req, secret: secret });
if (token) {
// Signed in
console.log("JSON Web Token", JSON.stringify(token, null, 2));
} else {
// Not Signed in
res.status(401);
}
res.end();
};
I have a request for authorization to the server:
axios
.post(
"http://localhost:5000/api/auth/sign-in",
{ ...values },
)
.then((res) => {
const { token } = res.data;
//there I need to save coookie with token that came from server
})
after successful authorization, the server sends the token, how can I save this token in the cookie with the httpOnly flag?
You can use universal-cookie package to do this: https://www.npmjs.com/package/universal-cookie
import Cookies from 'universal-cookie';
const cookies = new Cookies();
...
.then((res) => {
const { token } = res.data;
cookies.set('token', token , { httpOnly: true, path: "/" });
})
import Cookies from 'universal-cookie';
const cookies = new Cookies();
cookies.remove('abc');
console.log(cookies.getAll());
It is still printing my abc cookie.
May be you need to do something like
cookies.remove('abc', { path: '/' });
More info here
Cookies need to have both path and domain appended to them to be removed. Try this:
cookies.remove("abc", {path: "/", domain: ".example.com"})
If you are setting the cookie on a response in a login route/controller in express backend for JWT and are using 'httpOnly' option, you are unable to access the token from the client/react, even when using a third party library like 'universal-cookie' or 'document.cookie'.
You will need to clear the cookie on the response from the backend e.g. when a user logs out in the logout controller as detailed below.
Front-end:
// React redux logout action
export const logout = () => async (dispatch) => {
try {
await axios.get('/api/auth/logout')
localStorage.removeItem('userInfo')
dispatch({ type: type.USER_LOGOUT })
} catch (error) {
console.log(error)
}
}
Backend:
const User = require('../../models/userModel')
const generateToken = require('../../utils/generateToken')
// #desc Auth user & get token
// #route POST /api/auth/login
// #access Public
const login = async (req, res) => {
const { email, password } = req.body
try {
const user = await User.findOne({ email })
if (user && (await user.verifyPassword(password))) {
let token = generateToken(user._id)
res.cookie('token', token, {
maxAge: 7200000, // 2 hours
secure: false, // set to true if your using https
httpOnly: true,
})
res.json({
_id: user._id,
name: user.name,
email: user.email,
isAdmin: user.isAdmin,
token: token,
})
} else {
res
.status(401)
.json({ success: false, message: 'Invalid email or password' })
}
} catch (error) {
res.status(500).json({ success: false, message: error.toString() })
}
}
// #desc Logout controller to clear cookie and token
// #route GET /api/auth/login
// #access Private
const logout = async (req, res) => {
// Set token to none and expire after 1 seconds
res.cookie('token', 'none', {
expires: new Date(Date.now() + 1 * 1000),
httpOnly: true,
})
res
.status(200)
.json({ success: true, message: 'User logged out successfully' })
}
module.exports = {
login,
logout,
}
I just add this for people who may have similar problem in future, just like I had today. This may be an issue with asynchronous actions. Setting, removing cookies is asynchronous.