How to get data from protected API route - next.js

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

Related

NEXTAUTH_URL not being recognized by NextAuth

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.

Pass NextAuth JWT to Apollo Client

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

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

Firebase Auth check with Vue-router

The problem is that the vue-router's beforeEnter is triggered earlier than the beforeCreate hook in the main.js and have second delay, while after reload the vuex action set the user in state. This results in the user being bounced to the login page.
How can I delay vue-router's beforeEnter check until vuex set the authorized user in state.
router.js
import Vue from 'vue'
import Router from 'vue-router'
import Home from '../views/Home.vue'
import auth from './auth'
Vue.use(Router)
export default new Router({
mode: 'history',
routes: [
{
path: '/',
name: 'home',
component: Home
},
{
path: '/reservations',
name: 'Reservations',
component: () => import('#/views/Reservation/Reservations.vue'),
beforeEnter: auth
}
]
})
auth.js
import { store } from "../store";
export default (to, from, next) => {
if(store.getters.user) {
next()
} else {
console.log('auth')
next('/signin')
}
}
main.js
import Vue from 'vue'
import App from './App.vue'
import router from './router/router'
import {store} from './store'
import firebase from 'firebase/app'
import 'firebase/app'
import config from './config'
new Vue({
router,
store,
render: h => h(App),
beforeCreate() {
firebase.initializeApp(config).firestore().settings({timestampsInSnapshots: true})
console.log('main')
this.$store.dispatch('authCheck')
}
}).$mount('#app')
store/user.js
import firebase from 'firebase/app'
import 'firebase/auth'
export default{
state: {
user: null
},
mutations: {
setUser (state, payload) {
state.user = payload
}
},
actions: {
authCheck({ commit }) {
commit('setLoading', true)
firebase.auth().onAuthStateChanged((user) => {
if(user){
commit('setUser', {
id: user.uid,
name: user.displayName,
email: user.email
})
commit('setLoading', false)
}
})
},
logout({commit}) {
firebase.auth().signOut()
commit('setUser', null)
}
},
getters: {
user (state) {
return state.user
}
}
}
In the console I see first 'auth' from beforeEnter check, and then 'main' from beforeCreate. How to trigger an 'authCheck' before router's check
In your auth.js file instead of monitoring the store, you should wait for the authStateChanged event, then when you receive the user from firebase do your processing then.
auth.js
export default (to, from, next) => {
firebase.auth().onAuthStateChanged(function (user) {
if (user)
next();
else
next('/signin');
});
}

Resources