MSAL can not get access token even the refresh token still valid - reactjs

Hi I am using MSAL to manage the auth part in my app, I am using ADFS, the access token duration is 5min and the refresh token is 1 hour, the problem is that after each 5min, I got a 401 from backen telling me that the access token is expired, I am using acquireTokenSilent :
import {
InteractionRequiredAuthError,
PublicClientApplication,
} from '#azure/msal-browser';
import { authorized } from './request';
const msalConfig = new PublicClientApplication({
auth: {
clientId: process.env.REACT_APP_AUTH_CLIENT_ID || '',
authority: process.env.REACT_APP_AUTH_AUTHORITY,
knownAuthorities: [process.env.REACT_APP_AUTH_KNOWN_AUTHORITIES || ''],
redirectUri: process.env.REACT_APP_AUTH_REDIRECT_URI,
postLogoutRedirectUri: process.env.REACT_APP_AUTH_REDIRECT_URI,
},
cache: {
cacheLocation: 'sessionStorage',
storeAuthStateInCookie: false,
},
telemetry: {
application: {
appName: 'SOC',
appVersion: '1.0.0',
},
},
});
export const loginRequest = {
scopes: ['User.Read', 'openid'],
};
export const getToken = (instance) =>
new Promise((resolve, reject) => {
const accounts = instance.getAllAccounts();
if (accounts.length > 0) {
const request = {
...loginRequest,
account: accounts[0],
};
instance
.acquireTokenSilent(request)
.then((response) => {
resolve(response.accessToken);
authorized(response.accessToken);
})
.catch((error) => {
if (error instanceof InteractionRequiredAuthError) {
instance.acquireTokenRedirect(request);
}
reject('Error while acquiring the token');
});
}
});
export const getIdToken = (instance) => {
const accounts = instance.getAllAccounts();
if (accounts.length > 0) {
const request = {
...loginRequest,
account: accounts[0],
};
return instance.acquireTokenSilent(request).then((response) => {
return response?.idToken;
});
}
};
export default msalConfig;
after getting the access token I pass it in headers in api calls using axios :
import axios, { AxiosInstance } from 'axios';
import { AxiosConfigs } from './Configs';
import msalInstance, { getToken } from './authProvider';
const request: AxiosInstance = axios.create({
timeout: AxiosConfigs.TIMEOUT,
baseURL: AxiosConfigs.BASE_URL,
});
request.interceptors.request.use(
async (config) => {
// Do something before request is sent
const baseURL = AxiosConfigs.BASE_URL;
const tempConf = { ...config, baseURL };
let authorization = getAuthorization();
if (!authorization) {
await getToken(msalInstance);
tempConf.headers!.Authorization = getAuthorization();
}
return tempConf;
},
(error) => Promise.reject(error),
);
const authorized = (token) => {
request.defaults.headers.common['Authorization'] = `Bearer ${token}`;
return request;
};
const getAuthorization = () => {
return request.defaults.headers.common['Authorization'];
};
export default request;
export { authorized, getAuthorization };

Related

ReactJs how to add interceptor in axios

I've been working on this for hours, and I have no idea where did it go wrong.
I want to have an axios interceptor for my ReactJs
this is my interceptor axiosHandler.js
import axios from "axios";
const axiosHandler = axios.create({
baseURL: process.env.REACT_APP_BASE_URL,
headers: {
Accept: "application/json",
},
});
axiosHandler.interceptors.request.use(
(config) => {
const token = localStorage.getItem("token");
if (token) {
config.headers["Authorization"] = "Bearer " + token;
}
return config;
},
(error) => {
Promise.reject(error);
}
);
//axiosHandler.interceptors.response
export default axiosHandler;
And here is how I use the handler in my other component
import axiosHandler from "../services/axiosHandler";
const getData = async () => {
await axiosHandler
.get(`/path`)
.then((response) => {
//do something
})
};
And I get an error of below
services_axiosHandler__WEBPACK_IMPORTED_MODULE_0_.get is not a function
I've read many other solutions, but I can't find the difference as how it leads to the error of mine.
Where do I put it wrong?
Thank you
inside axios.index
import axios from "axios";
import { API_URL } from "../config/config";
const axiosHttp = axios.create({
baseURL: `${API_URL}`,
});
axiosHttp.interceptors.request.use(
(config) => {
const token = "Your Token here"
return {
...config,
headers: {
...(token !== null && { Authorization: `${token}` }),
...config.headers,
},
};
},
(error) => {
return Promise.reject(error);
}
);
axiosHttp.interceptors.response.use(
(response) => {
//const url = response.config.url;
//setLocalStorageToken(token);
return response;
},
(error) => {
if (error.response.status === 401) {
//(`unauthorized :)`);
//localStorage.removeItem("persist:root");
//removeLocalStorageToken
//window.location.href = "/login";
}
return Promise.reject(error);
}
);
export default axiosHttp;
Then inside your API function use it like below
import axiosHttp from "./utils/axios";
const getData = async ()=>{
try{
const response = await axiosHttp.get('/path')
return resposne;
}
catch(error){
//handle error here...
}
}
Last but not least, you shouldn't use await when using callback (then/catch)

How to correctly use RTK Query (SSR) with Next.js to include tokens stored in cookies

I have an application that uses Next.js, Redux, RTK Query and next-redux-wrapper and I'm having an issue with cookies not being available in a Next.js API route once store.dispatch(getMedications.initiate()) runs - the cookie is undefined on server render, but can be read fine once the page loads.
On the same page I have a useGetMedicationsQuery hook that runs, which works completely fine and can access the cookies when the query is run, however whenever the store.dispatch(getMedications.initiate()) query is run server side i.e. in the getServerSideProps the token cookie is undefined.
/pages/api/medications/get-medications.js
import axios from 'axios';
export default async (req, res) => {
const SERVICE_HOST = process.env.SERVICE_HOST;
const cookies = req.cookies;
const token = cookies.token; // undefined when initiated via store.dispatch(getMedications.initiate())
try {
const response = await axios.get(`${SERVICE_HOST}/patient/medications`, {
headers: {
'Authorization': `Bearer ${token}`
}
});
res.status(200).json(response.data.data);
} catch(error) {
res.status(500).json(error.response);
}
}
/services/medications.js
import { createApi, fetchBaseQuery } from "#reduxjs/toolkit/dist/query/react";
import { HYDRATE } from "next-redux-wrapper";
export const medicationApi = createApi({
reducerPath: "medicationApi",
baseQuery: fetchBaseQuery({
baseUrl: 'http://localhost:3001/api/medications/',
}),
keepUnusedDataFor: 3600,
extractRehydrationInfo(action, { reducerPath }) {
if (action.type === HYDRATE) {
return action.payload[medicationApi];
}
},
tagTypes: ['Medications'],
endpoints: (build) => ({
getMedications: build.query({
query: () => `get-medications/`,
providesTags: () => ['Medications']
}),
}),
})
export const {
useGetMedicationsQuery,
util: { getRunningOperationPromises }
} = medicationApi;
export const { getMedications } = medicationApi.endpoints;
/pages/medications.js
export const getServerSideProps = wrapper.getServerSideProps(
(store) => async ({ req, res }) => {
const WEBSITE_URL = process.env.WEBSITE_URL;
const SERVICE_HOST = process.env.SERVICE_HOST;
const COOKIE_DOMAIN = process.env.COOKIE_DOMAIN || '';
const cookies = cookie.parse(req.headers.cookie || '');
const token = cookies.token;
if (!token) {
return {
redirect: {
destination: `${WEBSITE_URL}/login/${queryString(req.url)}`,
permanent: false,
}
}
}
try {
... some stuff
store.dispatch(getMedications.initiate());
await Promise.all(getRunningOperationPromises());
return {
props: {
}
}
} catch(error) {
createTokenCookie(res, COOKIE_DOMAIN, '')
return {
redirect: {
destination: `${WEBSITE_URL}/login/`,
permanent: false,
}
}
}
}
);
extractRehydrationInfo(action, { reducerPath }) {
if (action.type === HYDRATE) {
return action.payload[medicationApi];
}
},
this should be changed to
extractRehydrationInfo(action, { reducerPath }) {
if (action.type === HYDRATE) {
return action.payload[reducerPath];
}
},

Session returns null Get request however in Post returns the session correctly in Next.js in server side (nextAuth)

I am trying to get the session data such as id, name, etc on the server-side but returns null in the terminal.
In the GET Method session is null, however, in POST method returns the session perfectly.
import { getSession } from "next-auth/react";
export default async (req, res) => {
const { method } = req;
switch (method) {
case "GET":
try {
const session = await getSession({ req });
console.log(JSON.stringify(session)); //Returns null
const jobs = await prisma.jobs.findMany();
return res.status(200).json({ success: true, jobs });
} catch (error) {
return res.status(404).json({
success: false,
message: error.message,
});
}
}
case "POST":
try {
const session = await getSession({ req });
console.log(JSON.stringify(session)); // here returns the session
if (!session || session.role != 2) {
return res.status(401).json({
msg: "You are not authorized to perform this action",
});
}
[...nextauth].js
...nextauth.js file applying credentials provider, I am not sure if I have to implement anything else in addition
import NextAuth from "next-auth";
import CredentialProvider from "next-auth/providers/credentials";
import axios from "axios";
export default NextAuth({
providers: [
CredentialProvider({
name: "credentials",
async authorize(credentials) {
try {
const user = await axios.post(
`${process.env.API_URL}/auth/authentication/login`,
{
email: credentials.email,
password: credentials.password,
}
);
if (!user.data.user) {
return null;
}
if (user.data.user) {
return {
id: user.data.user.id,
name: user.data.user.name,
surname: user.data.user.surname,
email: user.data.user.email,
role: user.data.user.role,
};
}
} catch (error) {
console.error(error);
}
},
}),
],
callbacks: {
jwt: ({ token, user }) => {
if (user) {
token.id = user.id;
token.name = user.name;
token.surname = user.surname;
token.email = user.email;
token.role = user.role;
}
return token;
},
session: ({ session, token }) => {
if (token) {
session.id = token.id;
session.name = token.name;
session.surname = token.surname;
session.email = token.email;
session.role = token.role;
}
return session;
},
},
secret: process.env.SECRET_KEY,
jwt: {
secret: process.env.SECRET_KEY,
encryption: true,
maxAge: 5 * 60 * 1000,
},
pages: {
signIn: "/auth/login",
},
});
This is weird. Can you try moving const session = await getSession({ req }) just before the switch statement?
...
const session = await getSession({ req });
console.log(JSON.stringify(session));
switch (method) {
...
}
....

Getting access token with the refresh token after expiration(JWT)

I get 401 error after a while when the page is reloaded, I figured it could be because the access token is expired. How do I set a new token with my refresh token? The below function runs every time the user visits a new page or refreshes the page. But it doesn't seem to work.
export async function currentAccount() {
if (store.get('refreshToken')) {
const query = {
grant_type: 'refresh_token',
companyId: store.get('lastCompanyId'),
refresh_token: store.get('refreshToken'),
}
const queryString = new URLSearchParams(query).toString()
const actionUrl = `${REACT_APP_SERVER_URL}/login?${queryString}`
return apiClient
.post(actionUrl, { auth: 'basic' })
.then(async response => {
if (response) {
const { access_token: accessToken } = response.data
store.set('accessToken', accessToken)
return response.data
}
return false
})
.catch(err => {
console.log('error', err)
store.clearAll()
})
}
return false
}
Login sets the access tokens
export async function login(email, password) {
const query = {
grant_type: 'password',
username: email,
password,
}
const queryString = new URLSearchParams(query).toString()
const actionUrl = `${REACT_APP_SERVER_URL}/login?${queryString}`
return apiClient
.post(actionUrl, { auth: 'basic' })
.then(async response => {
if (response) {
const {
data: {
access_token: accessToken,
refresh_token: refreshToken,
},
} = response
const decoded = jsonwebtoken.decode(accessToken)
response.data.authUser = decoded.authUser
const { userId, profileId, companyId } = decoded.authUser
if (accessToken) {
store.set('accessToken', accessToken)
store.set('refreshToken', refreshToken)
}
return response.data
}
return false
})
.catch(err => console.log(err))
}
saga users.js
export function* LOAD_CURRENT_ACCOUNT() {
yield put({
type: 'user/SET_STATE',
payload: {
loading: true,
},
})
const { authProvider } = yield select((state) => state.settings)
const response = yield call(mapAuthProviders[authProvider].currentAccount)
if (response) {
const decoded = jsonwebtoken.decode(response.access_token)
response.authUser = decoded.authUser
yield store.set('id', id)
try {
const user = yield call(LoadUserProfile)
if (user) {
const { company } = user
yield put({
type: 'user/SET_STATE',
payload: {
...user,
preferredDateFormat: user.preferredDateFormat || 'DD/MM/YYYY',
userId,
id,
},
})
}
} catch (error) {
}
}else{
store.set('refreshToken', response.refreshToken)
}
yield put({
type: 'user/SET_STATE',
payload: {
loading: false,
},
})
}
You can get a new access token with your refresh token using interceptors. Intercept and check for response status code 401, and get a new access token with your refresh token and add the new access token to the header.
Example:
return apiClient
.post(actionUrl, { auth: 'basic' })
.then(async response => {
if (response) { // check for the status code 401 and make call with refresh token to get new access token and set in the auth header
const { access_token: accessToken } = response.data
store.set('accessToken', accessToken)
return response.data
}
return false
});
Simple Interceptor example,
axios.interceptors.request.use(req => {
req.headers.authorization = 'token';
return req;
});
Interceptor example for 401
axios.interceptors.response.use(response => response, error => {
if (error.response.status === 401) {
// Fetch new access token with your refresh token
// set the auth header with the new access token fetched
}
});
There are several good posts on Interceptors usage for getting a new access token with your refresh token
https://thedutchlab.com/blog/using-axios-interceptors-for-refreshing-your-api-token
https://medium.com/swlh/handling-access-and-refresh-tokens-using-axios-interceptors-3970b601a5da
Automating access token refreshing via interceptors in axios
https://stackoverflow.com/a/52737325/8370370
The above answer is good. But I found below method is better than that also using Axios Interceptors and "jwt-decode". Give it a try. (I'm using session storage for this example. You can use your own way to store the tokens securely)
Methodology
Login to get an access token and long live refresh token and then store them securely.
Create an axios instance to check the access token expiration with "jwt-decode". Then add the access token into the request if there is a valid access token, or else request a new access token using the stored refresh token and then apply the new access token into the request.
Login:
import axios from 'axios'
const handleLogin = async (login) => {
await axios
.post('/api/login', login, {
headers: {
'Content-Type': 'application/json'
}
})
.then(async response => {
sessionStorage.setItem('accessToken', response.data.accessToken)
sessionStorage.setItem('refreshToken', response.data.refreshToken)
})
.catch(err => {
if (errorCallback) errorCallback(err)
})
}
Create axios instance:
import axios from 'axios'
import jwt_decode from 'jwt-decode'
import dayjs from 'dayjs'
const axiosInstance = axios.create({
headers: { 'Content-Type': 'application/json' }
})
axiosInstance.interceptors.request.use(async req => {
const accessToken = sessionStorage.getItem('accessToken') ? sessionStorage.getItem('accessToken') : null
if (accessToken) {
req.headers.Authorization = `Bearer ${accessToken}`
}
const tokenData = jwt_decode(accessToken)
const isExpired = dayjs.unix(tokenData.exp).diff(dayjs()) < 1
if (!isExpired) return req
const refreshToken = sessionStorage.getItem('refreshToken')
const response = await axios.post('/api/refresh', { refreshToken }, {
headers: {
'Content-Type': 'application/json'
}
})
req.headers.Authorization = `Bearer ${response.data.accessToken}`
sessionStorage.setItem('accessToken', response.data.accessToken)
return req
})
export default axiosInstance
Use axios instance in all the requests (Redux Toolkit Example):
import { createSlice, createAsyncThunk } from '#reduxjs/toolkit'
// Import axiosInstance
import axiosInstance from 'src/utils/axiosInstance'
export const getItems = createAsyncThunk(
'appItems/getItems',
async (args, { rejectedWithValue }) => {
try {
const response = await axiosInstance.get('/api/items')
return response.data
} catch ({ response }) {
return rejectedWithValue({ code: response.status, ...response.data })
}
}
)

Axios making 2 requests on refresh

When I navigate using Link (react router-dom) I don't have this problem, but if I refresh the browser I get a 403 error in console saying unauthorised and then I get the data in the next request with a 200 response. Why is this making what looks like 2 requests when refreshing the browser?
import { AuthContext } from "../../shared/context/auth-context";
const ContactEntries = () => {
const auth = useContext(AuthContext);
useEffect(() => {
const source = Axios.CancelToken.source();
setIsLoading(true);
const getContactEnquiries = async () => {
try {
const response = await Axios.get(
`${process.env.REACT_APP_BACKEND_URL}/v1/contact`,
{
cancelToken: source.token,
headers: { Authorization: "Bearer " + auth.token },
}
);
if (response.status === 200) {
setIsLoading(false);
setEnquiries(response.data.enquiries);
}
} catch (err) {
setIsLoading(false);
console.log(err.response);
}
};
getContactEnquiries();
return () => {
source.cancel();
};
}, [!!auth.token]);
}
Here is my authContext:
import { createContext } from "react";
export const AuthContext = createContext({
isLoggedIn: false,
userId: null,
token: null,
email: null,
firstName: null,
login: () => {},
logout: () => {},
});
This is because your useEffect is running twice on refresh. On first render it is not getting auth.token and may be it null. And on second render it is making call with 200 status code.
You have to check auth token it coming successfully.
You can check it this way
useEffect(() => {
const source = Axios.CancelToken.source();
setIsLoading(true);
const getContactEnquiries = async () => {
try {
const response = await Axios.get(
`${process.env.REACT_APP_BACKEND_URL}/v1/contact`,
{
cancelToken: source.token,
headers: { Authorization: "Bearer " + auth.token },
}
);
if (response.status === 200) {
setIsLoading(false);
setEnquiries(response.data.enquiries);
}
} catch (err) {
setIsLoading(false);
console.log(err.response);
}
};
if(auth.token) getContactEnquiries();
return () => {
source.cancel();
};
}, [!!auth.token]);

Resources