Using NextAuth for GraphQL authentication with Apollo client in Next.js encounter the error
Hooks can only be called inside of the body of a function.
import NextAuth from 'next-auth';
import Providers from 'next-auth/providers';
import { useMutation, useApolloClient } from '#apollo/client';
import { LOGIN_MUTATION } from '../../../graphql/mutations';
import { getErrorMessage } from '../../../lib';
export default (req, res) =>
NextAuth(req, res, {
providers: [
Providers.Credentials({
name: 'Credentials',
credentials: {
identifier: { label: "Email", type: "text" },
password: { label: "Password", type: "password" }
},
authorize: async (credentials) => {
const client = useApolloClient();
const [errorMsg, setErrorMsg] = useState();
const [login] = useMutation(LOGIN_MUTATION);
try {
await client.resetStore();
const { data: { login: { user, jwt } } } = await login({
variables: {
identifier: credentials.identifier,
password: credentials.password
}
});
if (user) {
return user;
}
} catch (error) {
setErrorMsg(getErrorMessage(error));
}
}
})
],
site: process.env.NEXTAUTH_URL || "http://localhost:3000",
session: {
jwt: true,
maxAge: 1 * 3 * 60 * 60,
updateAge: 24 * 60 * 60,
},
callbacks: {},
pages: {
signIn: '/auth/signin'
},
debug: process.env.NODE_ENV === "development",
secret: process.env.NEXT_PUBLIC_AUTH_SECRET,
jwt: {
secret: process.env.NEXT_PUBLIC_JWT_SECRET,
}
});
I am wondering is there anyway to make this work with apollo?
Thank you for the helps.
As in the comments rightfully pointed out, you can't use hooks in server-side code. You would have to create a new ApolloClient like this:
const client = new ApolloClient()
Then you can do queries like this for example:
const { data } = await client.query({
query: "Your query",
variables: { someVariable: true }
});
Best would be the to move the creation of the client to a separate external file as a function and import it in your server-side code whenever needed. Like done here for example.
Edit:
As #rob-art correctly remarks in the comments, for a [mutation][2], the code should look more like this:
const { data } = await client.mutate({
mutation: "Your query",
variables: { someVariable: true }
});
Related
I am using Django and Next.js (Version 13 with the app dir enabled). Now I have two questions:
What is the best practice to deal with the access token I receive after I do the authorize call to the django backend? Is it correct how I put it into the callbacks?
export const authOptions = {
secret: process.env.NEXTAUTH_SECRET,
providers: [
CredentialsProvider({
name: 'Django',
credentials: {
username: { label: "Username", type: "text", placeholder: "mail#domain.com" },
password: { label: "Password", type: "password" }
},
async authorize(credentials, req) {
// Do access call
const resToken = await fetch(process.env.AUTH_ENDPOINT, {
method: 'POST',
body: JSON.stringify(credentials),
headers: { "Content-Type": "application/json" }
})
const jwt_token = await resToken.json()
// fetching user data
const resUser = await fetch(`${process.env.BACKEND_URL}/auth/users/me/`, {
method: 'GET',
headers: { "Content-Type": "application/json",
"Authorization": `JWT ${jwt_token.access}` }
})
const user = await resUser.json()
if (resUser.ok && jwt_token.access) {
user.access_token = jwt_token.access
user.refresh_token = jwt_token.refresh
return user
}
// Return null if user data could not be retrieved
return null
}
})
],
session: {
strategy: "jwt",
},
jwt: { encryption: true, },
callbacks: {
async jwt({ token, user }) {
if (user) {
token.access_token = user.access_token
token.refresh_token = user.refresh_token
console.log("if executed")
}
return token
},
async session({ session, token, user }) {
if (!session) {
session.access_token = user.access_token
session.refresh_token = user.refresh_token
session.user = user
}return session;
},
}
}
export default NextAuth(authOptions)
I have the provider wrapped in the provider.js file as shown below. Now I was wondering if I need to passt the session as <SessionProvider session={session}> in the code below? And if yes - could you tell me how?
'use client'
import { SessionProvider } from 'next-auth/react'
export function Providers({ children }) {
return (
<SessionProvider>
{children}
</SessionProvider>
);
}
Thank you!
I deployed a Nextjs(v13) app with AWS Amplify and using NextAuth(v4.17.0). I'm using CredentialsProvider with a custom server. All works great in development environment, but in production the session callback doesn't fire and the session is empty, even if the token gets created in the database
/page/api/auth/[...nextauth].tsx disregard the console logs lol
import NextAuth from "next-auth";
import CredentialsProvider from "next-auth/providers/credentials";
import jwt_decode from "jwt-decode";
import { TokenInfo } from "../../../components/types/auth_types";
async function refreshAccessToken(token) {
try {
console.log("BUT WHY?");
const res = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/token/refresh/`,
{
method: "POST",
body: JSON.stringify({refresh: token.refreshToken}),
headers: {"Content-Type": "application/json"},
}
);
if (!res.ok) throw "refreshError";
const responseJson = await res.json();
return {
...token,
accessToken: responseJson.access,
}
} catch(error) {
return {
...token,
error: "RefreshAccessTokenError",
}
}
}
export const authOptions = {
providers: [
CredentialsProvider({
id: "credentials",
name: "Credentials",
credentials: {
email: { label: "Username", type: "text", placeholder: "" },
password: { label: "Password", type: "password" }
},
async authorize(credentials, req) {
const userCredentials = {
email: credentials.email, password: credentials.password
};
try {
const res = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/api/token/`,
{
method: "POST",
body: JSON.stringify(userCredentials),
headers: {"Content-Type": "application/json"},
credentials: "include",
}
);
console.log("res", res);
if (res.ok) {
const responseJson = await res.json();
console.log("resJson", responseJson);
const tokenInfo: TokenInfo = jwt_decode(responseJson.access);
console.log("tokenInfo", tokenInfo);
return {
id: tokenInfo.user_id.toString(),
email: tokenInfo.email,
firstName: tokenInfo.first_name,
lastName: tokenInfo.last_name,
isStaff: tokenInfo.is_staff,
accessToken: responseJson.access,
refreshToken: responseJson.refresh,
};
}
return null;
} catch(e) {
return null;
}
}
})
],
callbacks: {
async jwt({ token, account, user }) {
if (account && user) {
console.log("got into token", user);
token.firstName = user.firstName;
token.lastName = user.lastName;
token.refreshToken = user.refreshToken;
token.accessToken = user.accessToken;
}
if (token.accessToken) {
console.log("got in this if instead")
const decodedToken: TokenInfo = jwt_decode(token.accessToken);
if (Date.now() < decodedToken.exp * 1000) {
console.log("got here, returned properly");
return token;
}
}
console.log("got here, not properly, why?");
return await refreshAccessToken(token);
},
async session({ session, token }) {
console.log("getting session");
session.user.firstName = token.firstName;
session.user.lastName = token.lastName;
session.accessToken = token.accessToken;
console.log("sess", session);
return session;
}
},
secret: process.env.NEXT_AUTH_SECRET,
session: {
maxAge: 2 * 24 * 60 * 60, // two days
}
};
export default NextAuth(authOptions);
I have searched as best as I could and couldn't find anything that I didn't do already.
My understanding is I don't need to set session: { strategy: "jwt"} since that's the default.
I have NEXT_AUTH_SECRET="mysecret", NEXT_PUBLIC_API_URL="https://www.backend_domain.com" and NEXTAUTH_URL="https://www.frontend_domain.com" set properly in .env.production and the API calls succeed, as well as the NextAuth calls return 200 status code, no errors in Amplify logs
Edit 1:
If I navigate to /api/auth/signin/credentials and use the default UI for login, the session gets created successfully
I am building a multi-tenant NextJS app that uses next-auth for account authentication, tRPC for API's, and postgresql for a data store.
I am trying to find a way to dynamically update/set/mutate a session value based on some client-side interaction
The approach I am taking is similar to the one described in this article:
a User is granted access to an Organization through a Membership
a User may have a Membership to >1 Organization
a User can change which Organization they are "logged in" to through some client-side UI.
When the user authenticates, I want to:
set session.user.orgId to some orgId (if they belong to an org)
When the user changes the org they are accessing through some client-side UI, I want to:
update session.user.orgId = newOrgId (validating they have proper permissions before doing so, of course).
I have searched the net for ways to update/mutate session values, and as far as I can tell, it's only possible using next-auth's callbacks:
...
callbacks: {
async session({ session, user, token }) {
// we can modify session here, i.e `session.orgId = 'blah'`
// or look up a value in the db and attach it here.
return session
},
...
}
However, there is no clear way to trigger this update from the client, outside of the authentication flow. I.E, if the user clicks to change their org in some UI, how do I validate the change + update the session value, without requiring the user to re-authenticate?
Hack into NextAuth's PrismaAdapter and trpc's createContext.
For example:
File: src/pages/api/auth/[...nextauth].ts
import NextAuth, { Awaitable, type NextAuthOptions } from "next-auth";
import { PrismaAdapter } from "#next-auth/prisma-adapter";
import type { AdapterSession, AdapterUser } from "next-auth/adapters";
import { prisma } from "../../../server/db/client";
import { MembershipRole } from "#prisma/client";
...
const adapter = PrismaAdapter(prisma);
adapter.createSession = (session: {
sessionToken: string;
userId: string;
expires: Date;
}): Awaitable<AdapterSession> => {
return prisma.user
.findUniqueOrThrow({
where: {
id: session.userId,
},
select: {
memberships: {
where: {
isActiveOrg: true,
},
select: {
role: true,
organization: true,
},
},
},
})
.then((userWithOrg) => {
const membership = userWithOrg.memberships[0];
const orgId = membership?.organization.id;
return prisma.session.create({
data: {
expires: session.expires,
sessionToken: session.sessionToken,
user: {
connect: { id: session.userId },
},
organization: {
connect: {
id: orgId,
},
},
role: membership?.role as MembershipRole,
},
});
});
};
// the authOptions to user with NextAuth
export const authOptions: NextAuthOptions = {
// Include user.id on session
callbacks: {
session({ session, user }) {
if (session.user) {
session.user.id = user.id;
}
return session;
},
},
adapter: adapter,
// Configure one or more authentication providers
providers: [
...
],
};
export default NextAuth(authOptions);
File: src/server/trpc/context.ts
import type { inferAsyncReturnType } from "#trpc/server";
import type { CreateNextContextOptions } from "#trpc/server/adapters/next";
import type { Session } from "next-auth";
import { getServerAuthSession } from "../common/get-server-auth-session";
import { prisma } from "../db/client";
type CreateContextOptions = {
session: Session | null;
};
/** Use this helper for:
* - testing, so we dont have to mock Next.js' req/res
* - trpc's `createSSGHelpers` where we don't have req/res
**/
export const createContextInner = async (opts: CreateContextOptions) => {
return {
session: opts.session,
prisma,
};
};
/**
* This is the actual context you'll use in your router
* #link https://trpc.io/docs/context
**/
export const createContext = async (opts: CreateNextContextOptions) => {
const { req, res } = opts;
const sessionToken = req.cookies["next-auth.session-token"];
const prismaSession = await prisma.session.findUniqueOrThrow({
where: {
sessionToken: sessionToken,
},
select: {
orgId: true,
},
});
const orgId = prismaSession.orgId;
// Get the session from the server using the unstable_getServerSession wrapper function
const session = (await getServerAuthSession({ req, res })) as Session;
const sessionWithOrg = {
session: {
user: {
// need this otherwise createContextInner doesn't accept for a possible null session.user.id
id: session?.user?.id || "",
orgId: orgId,
...session?.user,
},
expires: session?.expires,
},
};
const context = await createContextInner(sessionWithOrg);
return context;
};
export type Context = inferAsyncReturnType<typeof createContext>;
I am deploying the application on Heroku, but not able to login while looking at the problem the keystone-session is rejected so added the secure and samesite attribute,but the issue remains same as it is not added to the cookie.
const sessionConfig = {
maxAge: 60 * 60 * 24 * 360, // How long they stay signed in?
secret: process.env.COOKIE_SECRET,
secure: true,
sameSite: 'none',
};
Error :
Some cookies are misusing the “SameSite“ attribute, so it won’t work as expected
Cookie “keystonejs-session” has been rejected because it is in a cross-site context and its “SameSite” is “Lax” or “Strict”.
keystone.js file
import { createAuth } from '#keystone-next/auth';
import { config, createSchema } from '#keystone-next/keystone/schema';
import {
withItemData,
statelessSessions,
} from '#keystone-next/keystone/session';
import { permissionsList } from './schemas/fields';
import { Role } from './schemas/Role';
import { OrderItem } from './schemas/OrderItem';
import { Order } from './schemas/Order';
import { CartItem } from './schemas/CartItem';
import { ProductImage } from './schemas/ProductImage';
import { Product } from './schemas/Product';
import { User } from './schemas/User';
import 'dotenv/config';
import { insertSeedData } from './seed-data';
import { sendPasswordResetEmail } from './lib/mail';
import { extendGraphqlSchema } from './mutations';
function check(name: string) {}
const databaseURL =
process.env.DATABASE_URL || 'mongodb://localhost/keystone-sick-fits-tutorial';
const sessionConfig = {
maxAge: 60 * 60 * 24 * 360, // How long they stay signed in?
secret: process.env.COOKIE_SECRET,
};
const { withAuth } = createAuth({
listKey: 'User',
identityField: 'email',
secretField: 'password',
initFirstItem: {
fields: ['name', 'email', 'password'],
// TODO: Add in inital roles here
},
passwordResetLink: {
async sendToken(args) {
// send the email
await sendPasswordResetEmail(args.token, args.identity);
},
},
});
export default withAuth(
config({
// #ts-ignore
server: {
cors: {
origin: [process.env.FRONTEND_URL],
credentials: true,
},
},
db: {
adapter: 'mongoose',
url: databaseURL,
async onConnect(keystone) {
console.log('Connected to the database!');
if (process.argv.includes('--seed-data')) {
await insertSeedData(keystone);
}
},
},
lists: createSchema({
// Schema items go in here
User,
Product,
ProductImage,
CartItem,
OrderItem,
Order,
Role,
}),
extendGraphqlSchema,
ui: {
// Show the UI only for poeple who pass this test
isAccessAllowed: ({ session }) =>
// console.log(session);
!!session?.data,
},
session: withItemData(statelessSessions(sessionConfig), {
// GraphQL Query
User: `id name email role { ${permissionsList.join(' ')} }`,
}),
})
);
I have a Nuxt app in which everything works fine in middleware except when I use redirect.
When I comment the redirect('/admin') line it works fine even the state data is present when console logged. As soon as I uncomment the redirect line it makes the state null.
Please help if someone knows this issue. This exact code works in my other projects but not here.
This is my auth.js file in the middleware folder.
export default function ({ store, route, redirect }) {
const user = store.getters['user/user']
const blockRouteAdmin = /\/admin\/*/g
const blockRouteManager = /\/manager\/*/g
const path = ['/signup', '/login']
let value = path.includes(route.path)
if (user) {
if (user.isAdmin) {
if (!route.path.match(blockRouteAdmin)) {
redirect('/admin')
}
}
if (user.isManager) {
if (!route.path.match(blockRouteManager)) {
redirect('/manager')
}
}
if (user.isUser) {
if (
route.path.match(blockRouteAdmin) ||
route.path.match(blockRouteManager) ||
value
) {
console.log('isUser', user.isUser)
redirect('/')
}
}
}
if (!user) {
if (
route.path.match(blockRouteAdmin) ||
route.path.match(blockRouteManager)
) {
redirect('/')
} else {
redirect()
}
}
}
Here is my nuxt.config.js
export default {
// Target: https://go.nuxtjs.dev/config-target
target: 'static',
// Global page headers: https://go.nuxtjs.dev/config-head
head: {
title: 'aitl',
meta: [
{ charset: 'utf-8' },
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
{ hid: 'description', name: 'description', content: '' },
{ name: 'format-detection', content: 'telephone=no' },
],
link: [{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }],
},
// Global CSS: https://go.nuxtjs.dev/config-css
css: [],
// Plugins to run before rendering page: https://go.nuxtjs.dev/config-plugins
plugins: ['~/plugins/firebaseConfig.js'],
// Auto import components: https://go.nuxtjs.dev/config-components
components: true,
// Modules for dev and build (recommended): https://go.nuxtjs.dev/config-modules
buildModules: [],
// Modules: https://go.nuxtjs.dev/config-modules
modules: [
// https://go.nuxtjs.dev/buefy
'nuxt-buefy',
// https://go.nuxtjs.dev/pwa
'#nuxtjs/pwa',
// https://go.nuxtjs.dev/content
'#nuxt/content',
],
// PWA module configuration: https://go.nuxtjs.dev/pwa
pwa: {
manifest: {
lang: 'en',
},
},
// Content module configuration: https://go.nuxtjs.dev/config-content
content: {},
// Build Configuration: https://go.nuxtjs.dev/config-build
build: {},
}
My index.js inside store.
import { vuexfireMutations } from 'vuexfire'
import { getUserFromCookie } from '../helper/index.js'
export const mutations = {
...vuexfireMutations,
}
export const actions = {
async nuxtServerInit({ dispatch, commit }, { req }) {
try {
const user = getUserFromCookie(req)
if (user) {
await dispatch('user/setUSER', {
email: user.email,
isAdmin: user.admin,
isManager: user.manager,
isUser: user.user,
uid: user.user_id,
name: user.name,
})
}
} catch (err) {
console.log(err)
}
},
}
User.js in store folder
import { auth } from '../plugins/firebaseConfig'
import Cookies from 'js-cookie'
export const state = () => ({
user: null,
})
export const getters = {
user(state) {
return state.user
},
}
export const actions = {
async userlogin({ dispatch }, user) {
try {
const token = await auth.currentUser.getIdToken(true)
const userInfo = {
email: user.email,
isAdmin: user.admin,
isManager: user.manager,
isUser: user.user,
uid: user.uid,
name: user.displayName,
}
Cookies.set('access_token', token)
await dispatch('setUSER', userInfo)
} catch (err) {
console.log(err)
}
},
setUSER({ commit }, user) {
commit('setUSER', user)
},
}
export const mutations = {
setUSER(state, user) {
state.user = user
},
}
The issue was solved by going from target: 'static' to target: 'server', aka mirroring the settings of another working project.