NEXTAUTH_URL not being recognized by NextAuth - next.js

I am having an issue with NextAuth in production. Every time I try to sign-in with Discord, after clicking the login button, it redirects me to localhost:3000/api/auth/signin/discord, while it should use the production URL instead.
I have NEXTAUTH_URL defined in the .env file, and I've attempted to pass the env value in the next.config.js file as well, but no luck.
Here is my current NextAuth handler;
import { NextAuthOptions } from "next-auth"
import NextAuth from "next-auth/next"
import DiscordProvider from "next-auth/providers/discord"
import { NextApiRequest, NextApiResponse } from "next/types"
function validateUser(userID: string | undefined) {
const allowedUsers = (process.env.ALLOWED_AUTH_IDS as string).split(",")
return userID && allowedUsers.includes(userID)
}
export const authOptions: NextAuthOptions = {
providers: [
DiscordProvider({
clientId: "<id>",
clientSecret: "<secret>",
}),
],
jwt: {
secret: process.env.SECRET_KEY,
maxAge: 30 * 24 * 60 * 60,
},
session: {
strategy: "jwt"
},
secret: process.env.SECRET_KEY,
callbacks: {
async jwt(props) {
return props.token
},
async session(props) {
if(props.token && props.session) {
props.session.user.authorized = validateUser(props.token.sub) as boolean
}
return props.session
},
async signIn(props) {
if(props.user) {
if(validateUser(props.user.id)) return true
}
return "/error/auth-not-allowed"
},
},
}
export default async function Auth(req: NextApiRequest, res: NextApiResponse) {
return await NextAuth(req, res, authOptions)
}
I have also attempted to use a custom redirect callback, but there was no luck there either. The baseUrl and url props of it were still http://localhost:3000 though.
So, to be clear, my goal is to make NextAuth use the production url instead of localhost. I deployed the application in my servers, and on Vercel, so NextAuth cannot fetch the URL automatically.

Related

How to get data from protected API route

I need to get data from protected API route. I am using Next Auth with google provider.
I have seen some solutions to similar problem but all of them were using JWT token. I don't use JWT. I don't have much experience with auth so I don't know what to pass to axios request.
This is /api/auth/[...nextauth].ts
import NextAuth, { NextAuthOptions } from 'next-auth'
import GoogleProvider from 'next-auth/providers/google'
import { PrismaAdapter } from '#next-auth/prisma-adapter'
import { prisma } from '../../../server/client'
export const authOptions: NextAuthOptions = {
adapter: PrismaAdapter(prisma),
providers: [
GoogleProvider({
clientId: process.env.GOOGLE_CLIENT_ID as string,
clientSecret: process.env.GOOGLE_CLIENT_SECRET as string,
}),
],
session: {
strategy: 'database',
},
}
export default NextAuth(authOptions)
This is how my route is protected:
const session = await getServerAuthSession({ req, res })
if (!session) {
return res.status(403).send({
error: 'You must be signed in to view the protected content on this page.',
})
}
This is get-server-auth-session.ts file
// Wrapper for unstable_getServerSession https://next-auth.js.org/configuration/nextjs
import type { GetServerSidePropsContext } from 'next'
import { unstable_getServerSession } from 'next-auth'
import { authOptions as nextAuthOptions } from '../../pages/api/auth/[...nextauth]'
// Next API route example - /pages/api/restricted.ts
export const getServerAuthSession = async (ctx: {
req: GetServerSidePropsContext['req']
res: GetServerSidePropsContext['res']
}) => {
return await unstable_getServerSession(
ctx.req,
ctx.res,
nextAuthOptions
)
}

NextAuth /api/auth/* always return 404 on Vercel deployment but work locally

Whenever I try to navigate to my sign-in page, it redirects to /api/auth/error on the vercel deployment. Locally, it navigates and works as expected.
From inspecting the network tab the first network request to fail is to /api/auth/providers
Not exactly sure where it's going wrong.
/pages/api/auth/[...nextauth].ts
import { PrismaAdapter } from '#next-auth/prisma-adapter';
import NextAuth from 'next-auth';
import GithubProvider from 'next-auth/providers/github';
import GoogleProvider from 'next-auth/providers/google';
import LinkedInProvider from 'next-auth/providers/linkedin';
import prisma from 'utils/prismaClient';
export default NextAuth({
providers: [
GithubProvider({
clientId: process.env.GITHUB_CLIENT_ID as string,
clientSecret: process.env.GITHUB_CLIENT_SECRET as string,
}),
GoogleProvider({
clientId: process.env.GOOGLE_CLIENT_ID as string,
clientSecret: process.env.GOOGLE_CLIENT_SECRET as string,
}),
LinkedInProvider({
clientId: process.env.LINKEDIN_CLIENT_ID as string,
clientSecret: process.env.LINKEDIN_CLIENT_SECRET as string,
}),
],
adapter: PrismaAdapter(prisma),
pages: {
signIn: '/sign-in',
error: '/sign-in',
},
});
/pages/sign-in.tsx:
type Props = {
providers: Provider[];
};
const SignIn: NextPage<Props> = ({ providers }) => {
const router = useRouter();
const [authCallbackUrl, setAuthCallbackUrl] = useState<string | undefined>(undefined);
const [error, setError] = useState<string | undefined>(undefined);
useEffect(() => {
if (!router.isReady) return;
setAuthCallbackUrl(('/' + router.query.callbackUrl) as string);
setError(router.query.error as string);
}, [router.isReady, router.query.callbackUrl, router.query.error]);
return (
<div>
<div>
{Object.values(providers).map((provider) => (
<button
key={provider.name}
type="button"
onClick={async () => {
await signIn(provider.id, {
callbackUrl: authCallbackUrl,
});
}}
>
{getSvgByProvider(provider.name)}
Sign in with {provider.name}{' '}
</button>
))}
</div>
{error && <SignInError error={error as keyof typeof SIGNIN_ERRORS} />}
</div>
);
};
export async function getServerSideProps() {
const providers = await getProviders();
return {
props: { providers },
};
}
export default SignIn;
Github Provider Callback Urls:
${VERCEL_URL}/api/auth/callback/github
I imagine there is nothing with my OAuth setup because it works locally? but not sure.
I've tried deploying with and without NEXTAUTH_URL in the env variables for Vercel but it has no effect as expected. According to documentation
When deploying here, you do not need to explicitly set the NEXTAUTH_URL environment variable.
Any idea what is going wrong? It works locally but when I deploy it, as soon as I call signIn(); from the homepage it navigates to api/auth/error
Edit: After console.logging providers inside the Sign-in page, it returns null. Any idea why that is the case?
Edit 2: Seems this is something to do with deploying it on Vercel? I deployed the same application on Netlify with NEXTAUTH_URL env variable defined and it works

getSession from NextAuth returns null in api if the call comes from getServerSideProps

I have have an NextJS application that uses some proxy rewrite to add auth headers from session to request header. This works fine on pages that do not use SSR.
When i try to make a request via apollo client in getServerSideProps, then getSession is suddenly null in the api route.
api route code:
import { getSession } from 'next-auth/react';
import httpProxyMiddleware from 'next-http-proxy-middleware';
import readDockerSecret from 'Utils/secret';
const middleware = async (req: NextApiRequest, res: NextApiResponse) => {
const session = await getSession({ req });
const token = session?.access_token;
if (!session) console.log('No session❤️‍🔥');
if (session) console.log('Got session🥑');
return httpProxyMiddleware(req, res, {
target: readDockerSecret('NEXUS_GRAPHQL_REWRITE_URL') || process.env.NEXUS_GRAPHQL_REWRITE_URL,
pathRewrite: {
'^/api/graphql': ''
// changeOrigin: true
// proxyTimeout: 5000
// secure: true
},
headers: { Authorization: `${token}` }
// headers: { Authorization: `${token}` }
});
};
export default middleware;```
Is there something i have to do to make sure i have a session when using ssr in nextjs (with nextauth)?

How to send httponly cookies client side when using next-auth credentials provider?

I'm creating a next js application, using next-auth to handle authentication.
I have an external backend api, so I'm using Credentials Provider.
The problem is that the backend sends httponly cookies, but those are not being attached to the browser when i make a request client side.
In /pages/api/[...auth].js
import NextAuth from 'next-auth';
import Providers from 'next-auth/providers';
import clientAxios from '../../../config/configAxios'
export default NextAuth({
providers: [
Providers.Credentials({
async authorize(credentials) {
try {
const login = await clientAxios.post('/api/login', {
username: credentials.username,
password: credentials.password,
is_master: credentials.is_master
})
const info = login.data.data.user
const token = {
accessToken: login.data.data.access_token,
expiresIn: login.data.data.expires_in,
refreshToken: login.data.data.refresh_token
}
// I can see cookies here
const cookies = login.headers['set-cookie']
return { info, token, cookies }
} catch (error) {
console.log(error)
throw (Error(error.response.data.M))
}
}
})
],
callbacks: {
async jwt(token, user, account, profile, isNewUser) {
if (token) {
// Here cookies are set but only in server side
clientAxios.defaults.headers.common['Cookie'] = token.cookies
}
if (user) {
token = {
user: user.info,
...user.token,
}
}
return token
},
async session(session, token) {
// Add property to session, like an access_token from a provider.
session.user = token.user
session.accessToken = token.accessToken
session.refreshToken = token.refreshToken
return session
}
},
session: {
jwt: true
}
})
my axios config file
import axios from 'axios';
const clientAxios = axios.create({
baseURL: process.env.backendURL,
withCredentials: true,
headers:{
'Accept' : 'application/json',
'Content-Type' : 'application/json'
}
});
export default clientAxios;
a page component
import { getSession } from "next-auth/client";
import clientAxios from "../../../config/configAxios";
import { useEffect } from "react"
export default function PageOne (props) {
useEffect(async () => {
// This request fails, cookies are not sent
const response = await clientAxios.get('/api/info');
}, [])
return (
<div>
<h1>Hello World!</h1>
</div>
)
}
export async function getServerSideProps (context) {
const session = await getSession(context)
if (!session) {
return {
redirect: {
destination: '/login',
permanent: false
}
}
}
// This request works
const response = await clientAxios.get('/api/info');
return {
props: {
session,
info: response.data
}
}
}
After time of researching I have figured it out.
I had to make a change in /pages/api/auth in the way I'm exporting NextAuth.
Instead of
export default NextAuth({
providers: [
...
]
})
Export it like this, so we can have access to request and response object
export default (req, res) => {
return NextAuth(req, res, options)
}
But to access them in the options object, we can make it a callback
const nextAuthOptions = (req, res) => {
return {
providers: [
...
]
}
}
export default (req, res) => {
return NextAuth(req, res, nextAuthOptions(req, res))
}
To send a cookie back to the frontend from the backed we must add a 'Set-Cookie' header in the respond
res.setHeader('Set-Cookie', ['cookie_name=cookie_value'])
The complete code would be
import NextAuth from 'next-auth';
import CredentialsProvider from 'next-auth/providers/credentials';
const nextAuthOptions = (req, res) => {
return {
providers: [
CredentialsProvider({
async authorize(credentials) {
try {
const response = await axios.post('/api/login', {
username: credentials.username,
password: credentials.password
})
const cookies = response.headers['set-cookie']
res.setHeader('Set-Cookie', cookies)
return response.data
} catch (error) {
console.log(error)
throw (Error(error.response))
}
}
})
]
}
}
export default (req, res) => {
return NextAuth(req, res, nextAuthOptions(req, res))
}
Update - Typescript example
Create a type for the callback nextAuthOptions
import { NextApiRequest, NextApiResponse } from 'next';
import { NextAuthOptions } from 'next-auth';
type NextAuthOptionsCallback = (req: NextApiRequest, res: NextApiResponse) => NextAuthOptions
Combining everything
import { NextApiRequest, NextApiResponse } from 'next';
import NextAuth, { NextAuthOptions } from 'next-auth';
import CredentialsProvider from 'next-auth/providers/credentials';
import axios from 'axios'
type NextAuthOptionsCallback = (req: NextApiRequest, res: NextApiResponse) => NextAuthOptions
const nextAuthOptions: NextAuthOptionsCallback = (req, res) => {
return {
providers: [
CredentialsProvider({
credentials: {
},
async authorize(credentials) {
try {
const response = await axios.post('/api/login', {
username: credentials.username,
password: credentials.password
})
const cookies = response.headers['set-cookie']
res.setHeader('Set-Cookie', cookies)
return response.data
} catch (error) {
console.log(error)
throw (Error(error.response))
}
}
})
],
callbacks: {
...
},
session: {
...
}
}
}
export default (req: NextApiRequest, res: NextApiResponse) => {
return NextAuth(req, res, nextAuthOptions(req, res))
}
To remove cookie in nextAuth after signing out, I used the following block of code - set the cookie parameters to match what you have for the cookie to be expired - Use the SignOut event in [...nextauth].js file
export default async function auth(req, res) {
return await NextAuth(req, res, {
...
events: {
async signOut({ token }) {
res.setHeader("Set-Cookie", "cookieName=deleted;Max-Age=0;path=/;Domain=.techtenum.com;");
},
},
...
}
}
You need to configure clientAxios to include cookies that the server sends as part of its response in all requests back to the server. Setting api.defaults.withCredentials = true; should get you what you want. See the axios configuration for my vue application below:
import axios from "axios";
export default ({ Vue, store, router }) => {
const api = axios.create({
baseURL: process.env.VUE_APP_API_URL
});
api.defaults.withCredentials = true; ------> this line includes the cookies
Vue.prototype.$axios = api;
store.$axios = api;
};

Trying to redirect from getStaticProps returns error during build

I'm currently trying to redirect inside of getStaticProps based on an API call which checks for a cookie. If the cookie is present, user is authenticated, which means no redirect, but if missing, redirect.
import React from "react";
import { GetStaticProps } from "next";
const Chat: React.FC = () => {
return null;
};
export const getStaticProps: GetStaticProps = async ({ locale, defaultLocale }) => {
const { authenticated } = await fetch("http://localhost:4000/api/auth").then(res => res.json());
if (!authenticated) {
return {
redirect: {
permanent: false,
destination: "/user/login",
},
};
}
return {
props: {
defaultLocale,
locale,
},
};
};
export default Chat;
This works fine during runtime, but on build, I get the following error:
Error: redirect can not be returned from getStaticProps during prerendering (/chat)
How so? The official next.js doc even shows how to redirect from getStaticProps. I even added the redirect to next.config.js, but somehow it still fails.
module.exports = {
async redirects() {
return [
{
source: "/chat",
destination: "/user/login",
permanent: false,
},
];
},
...
}
FYI the application uses i18n to danle translations, locales etc.

Resources