Pass NextAuth JWT to Apollo Client - next.js

I am creating a nextjs app that utilized NextAuth for authentication and the generating of a JWT with custom encode and decode. The custom encode and decode is required for passing hasura claims.
How do I pass that jwt to apollo in order to append the request headers with said jwt so that hasura authenticates my requests? I looked at a few examples where people were pulling their token from ocal storage but NEXTAuth stores the JWT in a cookie. I have no idea how to access it.
I have tried adding the token to my session and reading the session with NextAuth getSession() method but it returns null.
lib\apollo.ts
import {
ApolloClient,
InMemoryCache,
ApolloLink,
HttpLink,
concat,
} from '#apollo/client'
import { WebSocketLink } from '#apollo/client/link/ws'
import { getSession } from 'next-auth/react'
// TODO - Replace URIs with environment variables
async function sesFunc() {
const sesh = await getSession()
return sesh
}
var ses = sesFunc()
console.log(`Session in apollo: ${JSON.stringify(ses)}`)
const graphLink = new HttpLink({
uri: 'http://localhost:8080/v1/graphql',
})
//GraphQL Relay Endpoint
const relayLink = new HttpLink({
uri: 'http://localhost:8080/v1beta1/relay',
})
const wsLink =
typeof window !== 'undefined'
? new WebSocketLink({
uri: 'ws://localhost:8080/v1/graphql',
options: {
reconnect: true,
},
})
: undefined
const authMiddleware = new ApolloLink((operation, forward) => {
//const { data: session, status } = useSession()
// console.log(`AuthMiddle Session: ${session}`)
operation.setContext(({ headers = {} }) => ({
headers: {
...headers,
Authorization:
'Bearer <token>',
},
}))
return forward(operation)
})
const apolloClient = new ApolloClient({
link: concat(
authMiddleware,
ApolloLink.split(
(operation) => operation.getContext().clientName === 'relayLink',
relayLink,
ApolloLink.split(
(operation) => operation.getContext().clientName === 'graphLink',
graphLink,
wsLink
)
)
),
cache: new InMemoryCache(),
})
export default apolloClient
_app.tsx
import '../styles/globals.css'
import type { AppProps } from 'next/app'
import { SessionProvider } from 'next-auth/react'
import IdleTimerContainer from '../components/IdleTimerContainer'
import Layout from '../components/Layout'
import { ApolloProvider } from '#apollo/client'
import apolloClient from '../lib/apollo'
function MyApp({ Component, pageProps: { session, ...pageProps } }: AppProps) {
return (
<SessionProvider session={session}>
<ApolloProvider client={apolloClient}>
<Layout>
<Component {...pageProps} />
</Layout>
</ApolloProvider>
<IdleTimerContainer />
</SessionProvider>
)
}
export default MyApp

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
)
}

How to define dataSources with #apollo/client and NextJS?

I'm trying to use NextJS with Apollo, but I can't understand where should I insert my dataSources, so I'd be able to access it through context in my resolvers (as stated in the docs).
Here's my apollo.ts (the same from NextJS with-typescript-graphql example):
import { IncomingMessage, ServerResponse } from 'http'
import { useMemo } from 'react'
import {
ApolloClient,
InMemoryCache,
NormalizedCacheObject,
} from '#apollo/client'
import resolvers from './resolvers'
import typeDefs from './schema'
let apolloClient: ApolloClient<NormalizedCacheObject> | undefined
export type ResolverContext = {
req?: IncomingMessage
res?: ServerResponse
}
function createIsomorphLink(context: ResolverContext = {}) {
if (typeof window === 'undefined') {
const { SchemaLink } = require('#apollo/client/link/schema')
const { makeExecutableSchema } = require('#graphql-tools/schema')
const schema = makeExecutableSchema({
typeDefs,
resolvers,
})
return new SchemaLink({ schema, context })
} else {
const { HttpLink } = require('#apollo/client')
return new HttpLink({
uri: '/api/graphql',
credentials: 'same-origin',
})
}
}
function createApolloClient(context?: ResolverContext) {
return new ApolloClient({
ssrMode: typeof window === 'undefined',
link: createIsomorphLink(context),
cache: new InMemoryCache(),
})
}
export function initializeApollo(
initialState: any = null,
// Pages with Next.js data fetching methods, like `getStaticProps`, can send
// a custom context which will be used by `SchemaLink` to server render pages
context?: ResolverContext
) {
const _apolloClient = apolloClient ?? createApolloClient(context)
// If your page has Next.js data fetching methods that use Apollo Client, the initial state
// get hydrated here
if (initialState) {
_apolloClient.cache.restore(initialState)
}
// For SSG and SSR always create a new Apollo Client
if (typeof window === 'undefined') return _apolloClient
// Create the Apollo Client once in the client
if (!apolloClient) apolloClient = _apolloClient
return _apolloClient
}
export function useApollo(initialState: any) {
const store = useMemo(() => initializeApollo(initialState), [initialState])
return store
}
I tried to put it alonside with typeDefs in the parameters for makeExecutable, but it's still undefined when I try to use it.
My confusion is because this setup doesn't use the new ApolloServer() constructor, as the example in the docs and I can't find some docs which relates to my setup.

Vue + Pinia + Firebase Authentication: Fetch currentUser before Route Guard

Recently I started to use Pinia as a global store for my Vue 3 Project. I use Firebase for the user authentication and am trying to load the current user before Vue is initialized. Ideally everything auth related should be in a single file with a Pinia Store. Unfortunately (unlike Vuex) the Pinia instance needs to be passed to the Vue instance before I can use any action and I believe that is the problem. On first load the user object in the store is empty for a short moment.
This is the store action that is binding the user (using the new Firebase Web v9 Beta) in auth.js
import { defineStore } from "pinia";
import { firebaseApp } from "#/services/firebase";
import {
getAuth,
onAuthStateChanged,
getIdTokenResult,
} from "firebase/auth";
const auth = getAuth(firebaseApp);
export const useAuth = defineStore({
id: "auth",
state() {
return {
user: {},
token: {},
};
},
actions: {
bindUser() {
return new Promise((resolve, reject) => {
onAuthStateChanged(
auth,
async (user) => {
this.user = user;
if (user) this.token = await getIdTokenResult(user);
resolve();
},
reject()
);
});
},
// ...
}})
and this is my main.js file
import { createApp } from "vue";
import App from "./App.vue";
import router from "./router";
import { createPinia } from "pinia";
import { useAuth } from "#/store/auth";
(async () => {
const app = createApp(App).use(router).use(createPinia());
const auth = useAuth();
auth.bindUser();
app.mount("#app");
})();
How can I set the user before anything else happens?
I figured it out. Had to register the router after the async stuff
//main.js
(async () => {
const app = createApp(App);
app.use(createPinia());
const { bindUser } = useAuth();
await bindUser();
app.use(router);
app.mount("#app");
})();

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;
};

Get token from cookie using Apollo Client and Next-Auth

can you help to check my code is there anyway that i can get access token from cookie and send it through apollo client? In my Next.js project i am using NextAuth for authentication. After user logged in I save user information and access token inside session. But i have know idea how can i get it and pass it with apollo client.
import { useMemo } from 'react'
import { ApolloClient, ApolloLink, InMemoryCache, createHttpLink } from '#apollo/client'
import { setContext } from '#apollo/client/link/context';
import { concatPagination } from '#apollo/client/utilities'
import merge from 'deepmerge'
import isEqual from 'lodash/isEqual'
export const APOLLO_STATE_PROP_NAME = '__APOLLO_STATE__'
let apolloClient
const CLIENT_URL =
process.env.NODE_ENV === 'production'
? process.env.API_END_POINT
: 'http://localhost:1337'
function createApolloClient() {
const httpLink = createHttpLink({
uri: `${CLIENT_URL}/graphql`,
credentials: 'same-origin'
});
const authLink = setContext((_, { headers }) => {
return {
headers: {
...headers,
authorization: token ? `Bearer ${token}` : ''
}
}
});
const cache = new InMemoryCache({
typePolicies: {
Query: {
fields: {
jobs: concatPagination(),
},
},
},
});
const client = new ApolloClient({
srMode: typeof window === 'undefined',
link: authLink.concat(httpLink),
cache
});
return client;
}
export function initializeApollo(initialState = null) {
const _apolloClient = apolloClient ?? createApolloClient()
// If your page has Next.js data fetching methods that use Apollo Client, the initial state
// gets hydrated here
if (initialState) {
// Get existing cache, loaded during client side data fetching
const existingCache = _apolloClient.extract()
// Merge the existing cache into data passed from getStaticProps/getServerSideProps
const data = merge(initialState, existingCache, {
// combine arrays using object equality (like in sets)
arrayMerge: (destinationArray, sourceArray) => [
...sourceArray,
...destinationArray.filter((d) =>
sourceArray.every((s) => !isEqual(d, s))
),
],
})
// Restore the cache with the merged data
_apolloClient.cache.restore(data)
}
// For SSG and SSR always create a new Apollo Client
if (typeof window === 'undefined') return _apolloClient
// Create the Apollo Client once in the client
if (!apolloClient) apolloClient = _apolloClient
return _apolloClient
}
export function addApolloState(client, pageProps) {
if (pageProps?.props) {
pageProps.props[APOLLO_STATE_PROP_NAME] = client.cache.extract()
}
return pageProps
}
export function useApollo(pageProps) {
const state = pageProps[APOLLO_STATE_PROP_NAME]
const store = useMemo(() => initializeApollo(state), [state])
return store
}

Resources