How can I protect multiple Next.js API routes in next-auth - next.js

I build a simple API in Next.js and I use next-auth for authentication.
So far I have to use something like this in every API route:
const session = await getSession({ req });
if (session) {
... do something ...
} else {
... send back a 401 status
}
This seems to go against the DRY principle. Is there a clever way to apply protection to a number of routes in one place, such as Laravel route groups?

Make a middleware!
Disregard the typing if your not using TS
import { NextApiRequest, NextApiResponse } from 'next/types'
import { getSession } from 'next-auth/client'
export const protect = async (
req: NextApiRequest,
res: NextApiResponse,
next: any
) => {
const session = await getSession({ req })
if (session) {
console.log(session)
next()
} else {
res.status(401)
throw new Error('Not authorized')
}
}

Create a middleware that gets the session otherwise returns 401.
See NextJS docs on api middleware.
You can also check out their example in the github repo.

Related

How to implement iron-session with session id

I am using iron-session, next-connect with nextjs in our webapp and one of the requirements is to publish analytics events from our frontend code, like page views, button clicks and other custom events. These events are stored in our database and used by our data analyst with PowerBI.
Our webapp takes a user on an onboarding journey, then once it's done, we create an account for the user and redirects to dashboard. For the onboarding part, we don't have a user id yet while in the dashboard, we already do. However, we want to be able to track the user journey in the webapp so we need an identifier that is persisted throughout the whole journey. Thus, we think of a session id with the iron-session.
Now iron-session doesn't have a concept of session id, so I am trying to implement it myself. The session id will be our identifier of the user in our events table.
Here is the withSession middleware used with next-connect
import { getIronSession } from "iron-session";
import type { IncomingMessage } from "http";
import type { NextApiRequest } from "next";
import { nanoid } from "nanoid";
import appConfig from "#/backend/app.config";
export const sessionOptions = {
password: appConfig.secret,
cookieName: appConfig.cookies.sessionToken.name,
cookieOptions: appConfig.cookies.sessionToken.options,
};
export async function withSession(
req: IncomingMessage | NextApiRequest,
res: any,
next: any
) {
const session = await getIronSession(req, res, sessionOptions);
if (!session.id) session.id = nanoid(32);
req.session = session;
await req.session.save();
return next();
}
declare module "iron-session" {
interface IronSessionData {
user?: { id: string };
id: string;
}
}
And a route that will use the middleware
const router = createRouter<NextApiRequest, NextApiResponse>()
.use(...([withSession, withLogger, withTenant] as const))
.get(async (req, res) => {
// Authenticate user
req.session.user = { id: userId };
await req.session.save();
return res.redirect("/");
});
export default router.handler();
Is this a correct implementation of the said requirement?
Some libraries implement a kind of session.regenerate() when a user perform signIn and signOut. Do I need to implement it too? If I do, I will lose the identifier that persists throughout the whole user journey.
since you are using typescript first define the type of session object
declare module "iron-session" {
interface IronSessionData {
nameOfSessionObject?: {
// in your implementation you were creating req.user and req.id
// you could overwrite the req properties
user?: { id: string };
// you can manually create on the server
id: string;
};
}
}
create a wrapper session function
export function withSession(handler: any) {
return withIronSessionApiRoute(handler, {
password: appConfig.secret,
cookieName: appConfig.cookies.sessionToken.name,
// Said in another way, the browser will not send a cookie with the secure attribute set over an unencrypted HTTP request
cookieOptions: appConfig.cookies.sessionToken.options,
})}
create the session object. you do not use getIronSession when creating a session.
you need that when you need to access to the session object in middleware
export default withSessio(
async (req: NextApiRequest, res: NextApiResponse) => {
if (req.method === "GET") {
try {
const sessionObj={....}
req.session.nameOfSessionObject={...sessionObj}
await req.session.save();
// whatever you want to return
return res.json(sessionObj);
} catch (error) {
console.error("error in verify post req", error);
// 422 Unprocessable Entity
res.status(422).send({ message: "Cannot create SESSION" });
}
} else if (req.method === "POST") {
try {
..HANDLE POST HERE
} catch (error) {
res.status(422).send({ message: "Cannot generate a SESSION" });
}
} else {
return res.status(200).json({ message: "Invalid api Route" });
}
}
);
now you can import above handler and connect with next-connect

How do I stop auth middleware in NextJS applying to server-side/pre-rendering requests?

I'm trying to require the user to be logged in to access certain routes.
I have added the following middleware, as per the docs, but am having difficulty getting it to work.
I thought the issue was down to the server-side pre-rendered page always being created while unauthenticated, but I'm no longer sure. Middleware should only run on client-side requests from a browser, right?
When I include the block marked with 🟡s, the redirect does not happen.
When it's removed, the redirect always happens, even if the user is logged in.
Note that we're using "next": "^12.3.3", and we're not ready to upgrade to next v13 yet.
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import * as auth from 'src/lib/auth';
const pathsNeedingAuth = [
'/dashboard$',
'/account/(password|reset|delete)$',
];
export default async function middleware(request: NextRequest) {
if (typeof window === 'undefined') { // 🟡
return NextResponse.next(); // 🟡
} // 🟡
const pathNeedsAuth = pathsNeedingAuth.some(
(path) => new RegExp(path).test(request.nextUrl.pathname),
);
if (!pathNeedsAuth) {
return NextResponse.next();
}
const isAuthed = await auth.isAuthenticated();
if (isAuthed) {
return NextResponse.next();
}
const url = request.nextUrl.clone();
url.searchParams.set('redirectUrl', url.pathname);
url.pathname = '/account/login';
return NextResponse.redirect(url);
}
export const config = {
matcher: [...pathsNeedingAuth],
};
Any help will be appreciated! I know the structure of the code is a little odd, but that's simply from changing it while trying various things.
Redirects are incompatible with next export.

NextAuth signIn pass parameter to callback

I am using NextAuth to enable users to sign up/in with their Google account and also to link their Google account to their current account on my site.
In order to differentiate between signing up and linking an account when already signed in, I want to pass an extra parameter to signIn that I can access in the signIn callback that will allow me to take the correct action. I have tried:
signIn("google", null, { linkAccount: "true" });
However, this is only passed into the signIn request as a query parameter and is not passed through to the callback. Is there any way I can make a custom argument accessible in the callback?
Edit: Including more code below.
Call to next-auth's signIn client API:
signIn("google", null { linkAccount: "true" });
[...nextauth.js]
import NextAuth from 'next-auth';
import GoogleProvider from 'next-auth/providers/google';
import axios from 'axios';
const authOptions = (req) => ({
providers: [
GoogleProvider({
clientId: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
}),
],
secret: "secret",
callbacks: {
async signIn({
user, account, profile, email, credentials
}) {
// GOAL: How can I specify to this endpoint that I am just linking an account?
let res = await axios.post('http://localhost:8000/users/third_party_sign_in', {
third_party_id: user.id,
email: user.email,
type: account.provider
justLink: true|false
}, { withCredentials: true })
let path;
if (res.data.action === "login") {
path = `/?action=${res.data.action}&id=${res.data.user_id}&email=${user.email}&third_party=${account.provider}`
} else if (res.data.action === "create") {
path = `/?action=${res.data.action}&name=${user.name}&email=${user.email}&third_party=${account.provider}&third_party_id=${user.id}`
}
return path;
},
async redirect({ url }) {
return Promise.resolve(url)
}
},
});
function testNextApiRequest(req) {
if (req.query.nextauth
&& req.query.nextauth.length === 2
&& req.query.linkAccount) {
/// logs for signIn API call but not for callback
console.log("QUERY PARAMS: ", req.query);
}
}
export default (req, res) => {
testNextApiRequest(req);
return NextAuth(req, res, authOptions(req));
}
I also spent a lot of time on this trying to figure out how to get a param in a callback when using the signIn function.
Here's the solution
call signIn like you were doing
signIn("google", null, { linkAccount: "true" });
Now in [...nextauth].ts you want to parse req.query BEFORE passing it to next-auth like so
authOptions is just a function that returns next-auth callbacks and config.
export default async function auth(req: NextApiRequest, res: NextApiResponse) {
console.log(req.query); // This will have your linkAccount param
return await NextAuth(req, res, authOptions(req, res));
}
Now that you have access to the params you can do whatever logic you want. This will only work for some callbacks like redirect. Trying to get the params in the session callback is still proving to be impossible for me.
It isn't great, honestly it's pretty bad if you do db queries you'll be slowing down all the requests but I think this is currently the best way to do it. Hope it helps!
More discussion here https://github.com/nextauthjs/next-auth/discussions/901

nextjs-vercel api routes not working in production and development [duplicate]

I'm new to Next.js and I'm trying to understand the suggested structure and dealing with data between pages or components.
For instance, inside my page home.js, I fetch an internal API called /api/user.js which returns some user data from MongoDB. I am doing this by using fetch() to call the API route from within getServerSideProps(), which passes various props to the page after some calculations.
From my understanding, this is good for SEO, since props get fetched/modified server-side and the page gets them ready to render. But then I read in the Next.js documentation that you should not use fetch() to all an API route in getServerSideProps(). So what am I suppose to do to comply to good practice and good SEO?
The reason I'm not doing the required calculations for home.js in the API route itself is that I need more generic data from this API route, as I will use it in other pages as well.
I also have to consider caching, which client-side is very straightforward using SWR to fetch an internal API, but server-side I'm not yet sure how to achieve it.
home.js:
export default function Page({ prop1, prop2, prop3 }) {
// render etc.
}
export async function getServerSideProps(context) {
const session = await getSession(context)
let data = null
var aArray = [], bArray = [], cArray = []
const { db } = await connectToDatabase()
function shuffle(array) {
var currentIndex = array.length, temporaryValue, randomIndex;
while (0 !== currentIndex) {
randomIndex = Math.floor(Math.random() * currentIndex);
currentIndex -= 1;
temporaryValue = array[currentIndex];
array[currentIndex] = array[randomIndex];
array[randomIndex] = temporaryValue;
}
return array;
}
if (session) {
const hostname = process.env.NEXT_PUBLIC_SITE_URL
const options = { headers: { cookie: context.req.headers.cookie } }
const res = await fetch(`${hostname}/api/user`, options)
const json = await res.json()
if (json.data) { data = json.data }
// do some math with data ...
// connect to MongoDB and do some comparisons, etc.
But then I read in the Next.js documentation that you should not use fetch() to all an API route in getServerSideProps().
You want to use the logic that's in your API route directly in getServerSideProps, rather than calling your internal API. That's because getServerSideProps runs on the server just like the API routes (making a request from the server to the server itself would be pointless). You can read from the filesystem or access a database directly from getServerSideProps. Note that this only applies to calls to internal API routes - it's perfectly fine to call external APIs from getServerSideProps.
From Next.js getServerSideProps documentation:
It can be tempting to reach for an API Route when you want to fetch
data from the server, then call that API route from
getServerSideProps. This is an unnecessary and inefficient approach,
as it will cause an extra request to be made due to both
getServerSideProps and API Routes running on the server.
(...) Instead, directly import the logic used inside your API Route
into getServerSideProps. This could mean calling a CMS, database, or
other API directly from inside getServerSideProps.
(Note that the same applies when using getStaticProps/getStaticPaths methods)
Here's a small refactor example that allows you to have logic from an API route reused in getServerSideProps.
Let's assume you have this simple API route.
// pages/api/user
export default async function handler(req, res) {
// Using a fetch here but could be any async operation to an external source
const response = await fetch(/* external API endpoint */)
const jsonData = await response.json()
res.status(200).json(jsonData)
}
You can extract the fetching logic to a separate function (can still keep it in api/user if you want), which is still usable in the API route.
// pages/api/user
export async function getData() {
const response = await fetch(/* external API endpoint */)
const jsonData = await response.json()
return jsonData
}
export default async function handler(req, res) {
const jsonData = await getData()
res.status(200).json(jsonData)
}
But also allows you to re-use the getData function in getServerSideProps.
// pages/home
import { getData } from './api/user'
//...
export async function getServerSideProps(context) {
const jsonData = await getData()
//...
}
You want to use the logic that's in your API route directly in
getServerSideProps, rather than calling your internal API. That's
because getServerSideProps runs on the server just like the API routes
(making a request from the server to the server itself would be
pointless). You can read from the filesystem or access a database
directly from getServerSideProps
As I admit, what you say is correct but problem still exist. Assume you have your backend written and your api's are secured so fetching out logic from a secured and written backend seems to be annoying and wasting time and energy. Another disadvantage is that by fetching out logic from backend you must rewrite your own code to handle errors and authenticate user's and validate user request's that exist in your written backend. I wonder if it's possible to call api's within nextjs without fetching out logic from middlewars? The answer is positive here is my solution:
npm i node-mocks-http
import httpMocks from "node-mocks-http";
import newsController from "./api/news/newsController";
import logger from "../middlewares/logger";
import dbConnectMid from "../middlewares/dbconnect";
import NewsCard from "../components/newsCard";
export default function Home({ news }) {
return (
<section>
<h2>Latest News</h2>
<NewsCard news={news} />
</section>
);
}
export async function getServerSideProps() {
let req = httpMocks.createRequest();
let res = httpMocks.createResponse();
async function callMids(req, res, index, ...mids) {
index = index || 0;
if (index <= mids.length - 1)
await mids[index](req, res, () => callMids(req, res, ++index, ...mids));
}
await callMids(
req,
res,
null,
dbConnectMid,
logger,
newsController.sendAllNews
);
return {
props: { news: res._getJSONData() },
};
}
important NOTE: don't forget to use await next() instead of next() if you use my code in all of your middlewares or else you get an error.
Another solution: next connect has run method that do something like mycode but personally I had some problems with it; here is its link:
next connet run method to call next api's in serverSideProps
Just try to use useSWR, example below
import useSWR from 'swr'
import React from 'react';
//important to return only result, not Promise
const fetcher = (url) => fetch(url).then((res) => res.json());
const Categories = () => {
//getting data and error
const { data, error } = useSWR('/api/category/getCategories', fetcher)
if (error) return <div>Failed to load</div>
if (!data) return <div>Loading...</div>
if (data){
// {data} is completed, it's ok!
//your code here to make something with {data}
return (
<div>
//something here, example {data.name}
</div>
)
}
}
export default Categories
Please notice, fetch only supports absolute URLs, it's why I don't like to use it.
P.S. According to the docs, you can even use useSWR with SSR.

How do I use API middlewares to protect API routes from unauthenticated users in Next.js?

I have a next.js app that has several API routes that I am hoping to protect from users who are not logged in. Using next-auth, I understand that I can add the following code to each API route to achieve this.
import { getSession } from 'next-auth/client'
export default async (req, res) => {
const session = await getSession({ req })
if (session) {
res.send({ content: 'This is protected content. You can access this content because you are signed in.' })
} else {
res.send({ error: 'You must be sign in to view the protected content on this page.' })
}
}
However, I was wondering if it is possible to use API middlewares, so I am not repeating the same code over and over again? I read through the Next.js API middlewares documentation (https://nextjs.org/docs/api-routes/api-middlewares) and did the following:
import Cors from 'cors';
import { getSession } from 'next-auth/react';
function initMiddleware(middleware) {
return (req, res) =>
new Promise((resolve, reject) => {
middleware(req, res, async (result) => {
const session = await getSession({ req });
if (!session) {
return reject(result);
}
return resolve(result);
});
});
}
const cors = initMiddleware(
Cors({
methods: ['GET', 'POST', 'OPTIONS'],
})
);
export default async function handler(req, res) {
await cors(req, res);
\* fetching from database *\
Although it works, the following error is returned when I tried to access the API route when unauthenticated, and it feels like I'm not doing it properly.
error - null
wait - compiling /_error (client and server)...
Error [ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the client
at ServerResponse.setHeader (_http_outgoing.js:561:11)
at DevServer.renderError (/Users/alextung/Desktop/Projects/askit/node_modules/next/dist/server/next-server.js:1677:17)
at DevServer.run (/Users/alextung/Desktop/Projects/askit/node_modules/next/dist/server/dev/next-dev-server.js:452:35)
at processTicksAndRejections (internal/process/task_queues.js:95:5)
at async DevServer.handleRequest (/Users/alextung/Desktop/Projects/askit/node_modules/next/dist/server/next-server.js:325:20) {
code: 'ERR_HTTP_HEADERS_SENT'
}
error - Error [ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the client
Would really appreciate some help on this given that this is my first time working with middlewares. Thank you!

Resources