I am using in my NextJS application Next-Auth for authentication to call a separate BE that gives me an access and refresh token. What I am trying to do with that tokens is to store them as cookies and use Axios interceptors to refresh the tokens when they are expired, I tried many ways to update the tokens but not successful.
What I have tried:
- call the cookies().set function in a server action but I always get "cookies can be modified from server action"
- added 2 api endpoints where I am trying to expire and save new tokens, they return successful but not result
The Axios instance is used in all my pages to fetch data. What I am trying to do with this is to always try to refresh tokens and if it doesnt work, to just log the user out.
What could I miss? (using app router)
Thank you in advance for any help!
options.tsx
import CredentialsProvider from 'next-auth/providers/credentials';
import { login } from '@/foundation/Authentication/OAuth2';
import Axios from '@/foundation/Axios';
import { JWT } from 'next-auth/jwt';
import { DefaultUser, Session } from 'next-auth';
import { User } from '@/foundation/Models/User';
import { cookies } from 'next/headers';
export const options = {
providers: [
CredentialsProvider({
name: 'Credentials',
credentials: {
username: { label: 'Username', type: 'text' },
password: { label: 'Password', type: 'password' }
},
async authorize(credentials): Promise<DefaultUser | null> {
if (!credentials?.username || !credentials?.password) {
return null;
}
try {
const tokens = await login(credentials.username, credentials.password);
cookies().set('accessToken', tokens.accessToken);
cookies().set('refreshToken', tokens.refreshToken);
const {
data: { data }
} = await Axios.get('/me');
return data;
} catch (err) {
return null;
}
}
})
],
pages: {
signIn: '/login'
},
callbacks: {
async jwt({ token, user }: { token: JWT; user: User }): Promise<JWT> {
if (user) {
token.id = user.id;
token.firstName = user.first_name;
token.lastName = user.last_name;
token.role = user.role;
}
return token;
},
async session({
session,
token
}: {
session: Session;
token: JWT;
}): Promise<Session> {
session.user.id = token.id;
session.user.first_name = token.firstName;
session.user.last_name = token.lastName;
session.user.role = token.role;
session.user.name = `${token.firstName} ${token.lastName}`;
return session;
}
}
};
Login method:
export const login = async (username: string, password: string) => {
try {
const token = await auth.owner.getToken(username, password);
const tokens = {
accessToken: token.accessToken,
refreshToken: token.refreshToken
};
// await setTokens(tokens.accessToken, tokens.refreshToken);
return tokens;
} catch (e) {
if (!AxiosStatic.isAxiosError(e)) {
throw e;
}
if (e.status !== 401) {
throw e;
}
return false;
}
};
Axios instance
import axios, { CancelToken } from 'axios';
import { isEmpty } from 'lodash';
import { getTokens, destroyCookies } from '@/foundation/Storage';
import { refresh } from '@/foundation/Authentication/OAuth2';
import { apiUrl } from '@/utils/Constants';
import { signOut } from 'next-auth/react';
const instance = axios.create({
baseURL: apiUrl,
headers: {
'content-type': 'application/json'
},
responseType: 'json'
});
instance.interceptors.request.use(
async (config) => {
// Axios bug: this ensures content-type is always set
if (!config.data) {
config.data = {};
}
let tokens = getTokens();
if (isEmpty(tokens.accessToken)) {
try {
await refresh();
// Cancel the me call, after refreshing tokens
if (config.url === '/me') {
config.cancelToken = new CancelToken((cancel) =>
cancel('Cancel repeated request')
);
}
} catch (errInner) {
config.cancelToken = new CancelToken((cancel) =>
cancel('failed to refresh tokens')
);
destroyCookies();
}
tokens = getTokens();
if (isEmpty(tokens.accessToken)) {
return config;
}
}
config.headers['authorization'] = `Bearer ${tokens.accessToken}`;
return config;
},
(err) => Promise.reject(err)
);
instance.interceptors.response.use(
(response) => response,
async (err) => {
const orgRequest = err.config;
// Only attempt to retry 401's that hasn't been retried before
if (err.response.status === 401 && !orgRequest._retry) {
orgRequest._retry = true;
let tokens = getTokens();
if (isEmpty(tokens.refreshToken)) {
return Promise.reject(err);
}
try {
await refresh();
// Cancel the me call, after refreshing tokens
if (orgRequest.url === '/me') {
orgRequest.cancelToken = new CancelToken((cancel) =>
cancel('Cancel repeated request')
);
}
} catch (errInner) {
destroyCookies();
return Promise.reject(err);
}
// Retry the request, with new tokens
return instance(orgRequest);
}
await signOut();
return Promise.reject(err);
}
);
export default instance;
Delete token example
'use server';
import { NextResponse } from 'next/server';
export async function DELETE() {
const response = new NextResponse(JSON.stringify({}), {
status: 200,
headers: {
'Content-Type': 'application/json'
},
});
response.cookies.set('accessToken', '', { path: '/', expires: new Date(0) });
response.cookies.set('refreshToken', '', { path: '/', expires: new Date(0) });
return response;
}
fetch
oraxios
to call your API and then set cookies on the client side usingdocument.cookie
or a client-side cookie management library. – Frown