How to make simple protected route using nextAuth? - reactjs

I wanna make simple protected route.
I have credentials provider and nextAuth middleware. I just wanna make simple logic:
if user is logged in he can visit /profile, and if he visits /signup or /signin redirect him to /profile, and if he isnt logged he cant visit /profile and redirect him to /signin
some routes are neutral - for example he can visit /shop while being logged in or not.
there is my [...nextauth].ts
export default NextAuth({
session: {
strategy: 'jwt',
},
providers: [
CredentialsProvider({
type: 'credentials',
async authorize(credentails) {
const { password, email } = credentails as Signin
try {
const client = await connectToDatabase()
if (!client) return
const db = client.db()
const user = await existingUser(email, db)
if (!user) throw new Error('Invalid credentails!')
const isPasswordCorrect = await verifyPassword(password, user.password)
if (!isPasswordCorrect) throw new Error('Invalid credentails!')
return { email: user.email, name: user.name, id: user._id.toString() }
} catch (e: unknown) {
if (e instanceof Error) {
throw new Error(e.message)
}
}
},
}),
],
})

Apart from other answers what you can do is-
At component mount at signin and sign up check user is authenticated or not. If authenticated. use router.push to profile else be at signin/signup.
At profile again check for authentiction at component mount, if not auth push to signin else be at profile. Important thing here is don't show the layout, content of profile page before checking user is authenticated or not. Use a spiner or loader till auth check is going on.

write a middleware
const authorizedRoles = (...roles) => {
return (req, res, next) => {
if (!roles.includes(req.user.role)) {
return next(
// write logic to handle errors
new ErrorHandler(
`Role (${req.user.role}) is not allowed`,
403
)
);
}
next();
};
};
then whichever routes you want to protect, use this middleware. Then on protected pages' getServerSideProps
export async function getServerSideProps(context) {
const session = await getSession({ req: context.req });
if (!session || session.user.role !== "admin") {
return {
redirect: {
destination: "/home",
// permanent - if `true` will use the 308 status code which instructs clients/search engines to cache the redirect forever.
permanent: false,
},
};
}
return {
props: {},
};
}

Related

next-auth 4 session returns null, next.js

This is code in api/auth/[...nextAuth].js
import NextAuth from "next-auth";
import CredentialsProvider from "next-auth/providers/credentials";
import User from "#/models/user";
import connectToDb from "#/config/dbConnection";
export default NextAuth({
session: {
strategy: "jwt",
},
providers: [
CredentialsProvider({
async authorize(credentials) {
connectToDb();
const { email, password } = credentials;
// check if email and passwird entered
if (!email || !password) {
throw new Error("please enter email or password");
}
const user = await User.findOne({
email,
// becasue in pre.save I excluded returning password in api response
}).select("+password");
if (!user) {
throw new Error("Invalid email or password");
}
// check if password is correct
// I added comparePassword to userSchema methods in mongoose
const isPasswordMatched = await user.comparePassword(password);
if (!isPasswordMatched) {
throw new Error("please enter email or password");
}
// creating a promise
return Promise.resolve(user);
},
}),
],
callbacks: {
// jwt callback is only called when token is created
jwt: async ({ token, user }) => {
// user is obj that we have received from authorize Promise.resolve(user)
user && (token.user = user);
// not this token has user property
return Promise.resolve(token);
},
// user arg here is actully token that returned from jwt.
session: async ({ session, token }) => {
// session callback is called whenever a session for that particular user is checked
console.log("user in ...next auth api", token);
session.user = token.user;
// since I get error, I return Promise.resolve(session)
return Promise.resolve(session);
},
},
});
then I write a middleware to get the session:
const isAuthenticatedUser = asyncErrors(async (req, res, next) => {
// when user logged in we create the session of user
// getSession gets auth-token from req.headers
const session = await getSession({ req });
// THIS RETURNS NULL
console.log("session", session);
if (!session) {
return next(new ErrorHandler("Login first to access this resource", 401));
}
req.user = session.user;
next();
});
export { isAuthenticatedUser };
I write api/me to check the user
import nc from "next-connect";
import connectToDb from "#/config/dbConnection";
import { currentUserProfile } from "#/controllers/authController";
import { isAuthenticatedUser } from "#/middlewares/auth";
import onError from "#/middlewares/errors";
const handler = nc({ onError });
connectToDb();
handler.use(isAuthenticatedUser).get(currentUserProfile);
export default handler;
When I visit http://localhost:3000/api/me, since session is logged as null, I get not authneticated, even though, in chrome debugging tools' Aplication tab next-auth credentials are present
Since https://next-auth.js.org/getting-started/upgrade-v4
Version 4 makes using the SessionProvider mandatory.
this is _app.js component
import { SessionProvider } from "next-auth/react"
export default function App({
Component,
pageProps: { session, ...pageProps },
}) {
return (
// `session` comes from `getServerSideProps` or `getInitialProps`.
// Avoids flickering/session loading on first load.
<SessionProvider session={session} refetchInterval={5 * 60}>
<Component {...pageProps} />
</SessionProvider>
)
}
This problem is happening due to not specifying e.preventDefault() on login button.
The working code should look like this :-
async function login(e) {
e.preventDefault(); //Add this to your code.
const getLoginStatus = await signIn("credentials", {
redirect: false,
username,
password,
})};

How to have NextJS Auth example automatically login after sign-up

The example in the NextJS repository uses cookie storage and iron-session in order to maintain auth during a user's logged in session. The example code is at this URL:
https://github.com/vercel/next.js/tree/canary/examples/with-passport
Unfortunately, it forces the user to first signup for a new account, then login with those credentials instead of automatically logging the user in when a successful signup is made.
It uses the NextJS API Routes, and the Passport Local strategy for authentication. I'm pasting the code below for both the sign up and the login routes.
I found some other SO posts that talked about how to use the authenticate method in login inside the signup method, but I believe that requires middleware that I don't understand. I can't just paste in the code from login into signup.
My question is, I want to have signup automatically create the cookie and then redirect to some other page in a logged in state.
/pages/api/signup.js
import { createUser } from '../../lib/user'
export default async function signup(req, res) {
try {
await createUser(req.body)
res.status(200).send({ done: true })
// WANTING TO ADD SOME CODE POSSIBLY HERE TO AUTO LOGIN
} catch (error) {
console.error(error)
res.status(500).end(error.message)
}
}
/pages/api/login.js
import passport from 'passport'
import nextConnect from 'next-connect'
import { localStrategy } from '../../lib/password-local'
import { setLoginSession } from '../../lib/auth'
const authenticate = (method, req, res) =>
new Promise((resolve, reject) => {
passport.authenticate(method, { session: false }, (error, token) => {
if (error) {
reject(error)
} else {
resolve(token)
}
})(req, res)
})
passport.use(localStrategy)
export default nextConnect()
.use(passport.initialize())
.post(async (req, res) => {
try {
const user = await authenticate("local", req, res);
// session is the payload to save in the token, it may contain basic info about the user
const session = { ...user };
await setLoginSession(res, session);
res.status(200).send({ done: true });
} catch (error) {
console.error(error);
res.status(401).send(error.message);
}
});

How to get session in NextJS api route using with-iron-session?

I am using with-iron-session for authentication in my NextJS app however I'm not able to access the session cookie when I make API calls in my getServerSideProps() function. The API route is unable to get the session cookie used for authentication:
Session creation:
// this file is a wrapper with defaults to be used in both API routes and `getServerSideProps` functions
import { withIronSession } from "next-iron-session";
export default function withSession(handler) {
return withIronSession(handler, {
password: process.env.PASSWORD_HASH,
cookieName: "MYCOOKIE",
cookieOptions: {
// the next line allows to use the session in non-https environements like
// Next.js dev mode (http://localhost:3000)
secure: process.env.NODE_ENV === "production",
httpOnly: false,
},
});
}
My getServerSideProps call:
export const getServerSideProps = withSession(async ({ req, res }) => {
const user = req.session.get("user");
if (!user) {
return {
redirect: {
permanent: false,
destination: "/"
},
props: {}
};
}
// I've replaced 'include' with 'same-origin' but it didn't make a difference
const watchRes = await fetch('/watch',{credentials: 'include'});
const watch = await watchRes.json();
return{
props: {
user,
watch
}
}
}
the api route:
// I've added await before the (req,res) but that was just guessing at this point
export default withSession((req, res) => {
const user = req.session.get("user");
if(user){
res.send("Good");
}else{
res.status(403).end();
}
}
While being logged in and I go to my localhost/api/watch route, it shows "Good" but when I try the fetch request I get the 403.
try to add headers of the getServerSideProps request to the second call.
const headers = req.headers
const watchRes = await fetch('/watch',{headers});
in this way you have injected the cookies

Server-side authorization with JWT in SvelteKit

I have an issue sending a JWT token to the server and using it to authorize access in load handlers. I am using Firebase on the client for authentication. When logged in (onAuthStateChanged), I send a POST request with the token to the /api/login endpoint:
export async function post(req) {
const idToken = req.headers['authorization']
try {
const token = await firebase().auth().verifyIdToken(idToken)
req.locals.user = token.uid
} catch (e) {
console.log(e)
return {
status: 500,
body: 'forbidden',
}
}
return {
status: 200,
body: 'ok',
}
}
In hooks.js:
export function getSession(request) {
return {
user: request.locals.user
}
}
export async function handle({ request, resolve }) {
const cookies = cookie.parse(request.headers.cookie || '')
request.locals.user = cookies.user
const response = await resolve(request)
response.headers['set-cookie'] = `user=${request.locals.user || ''}; Path=/; HttpOnly`
return response
}
In load methods:
export async function load({ session }) {
if (!session.user) {
return {
status: 302,
redirect: '/start'
}
}
// ...
}
All of this works fine except that any client-side navigation after a login is rejected because session.user is still undefined. When navigating by typing the URL in the browser, it works correctly and after that the client-side navigation also works.
Any ideas why and what to do?
I have solved this by adding a browser reload on whichever page the user lands on after logging in. The snippet for the reload on the client side handling on a successful response from the login API endpoint looks like this
if (sessionLoginResponse?.status === "success") {
await signOut(auth);
window.history.back();
setTimeout(() => {
window.location.reload();
}, 10);
}

Persist auth state in react/react native for Firebase

I am using react native for an ios app and firebase for authentication. Every time I leave the app and come back, it asks for a login. I want to persist the firebase login but don't really know where to put it.
I know I need to put this in:
firebase.auth().setPersistence(firebase.auth.Auth.Persistence.LOCAL)
I have the following signIn function that runs when the login button is pressed on the signInScreen:
const signIn = async () => {
setLoading(true);
try {
await firebase.signIn(email, password);
const uid = firebase.getCurrentUser().uid;
const userInfo = await firebase.getUserInfo(uid);
const emailArr = userInfo.email.split("#");
setUser({
username: emailArr[0],
email: userInfo.email,
uid,
isLoggedIn: true,
});
} catch (error) {
alert(error.message);
} finally {
isMounted.current && setLoading(false);
}
};
I have the following signIn stuff in my firebaseContext:
const Firebase = {
getCurrentUser: () => {
return firebase.auth().currentUser;
},
signIn: async (email, password) => {
return firebase.auth().signInWithEmailAndPassword(email, password);
},
getUserInfo: async (uid) => {
try {
const user = await db.collection("users").doc(uid).get();
if (user.exists) {
return user.data();
}
} catch (error) {
console.log("Error #getUserInfo", error);
}
},
logOut: async () => {
return firebase
.auth()
.signOut()
.then(() => {
return true;
})
.catch((error) => {
console.log("Error #logout", error);
});
},
};
Where do I put the persist code I listed above from the docs?
Thanks!
When do you check if someon is signed in or not?
From the code shown it looks like you check it manuelly by calling currentUser. You have to consider that the persistance of auth state is asynchronous. That means if you call currentUser on auth before the localy saved auth state is loaded you would get there null and thing that the user is not signed in.
To get the auth state Firebase recommend to use the onAuthStateChanges event listener. With that you can listen to auth state changes no matter if you logged in or the persistet auth state is loaded.
The usage is very simple:
firebase.auth().onAuthStateChanged(function(user) {
if (user) {
// User is signed in.
} else {
// No user is signed in.
}
});
That is the reson I asked where you check if someon is signed in or not. If I could see that code I could help you adopt it to use that event listener.

Resources