React-Apollo msal-browser with msal-react wrapper does not get accounts - reactjs

I am moving my package from react-adal to #azure/msal-react. In react-adal I can authorise and able to go my app. I am using same client_id and Tenant_id but seems like getAllAccounts() returns me empty array, it means no user found as a result I am not getting any token. I used exactly same what the doc says. I am not sure what I am making mistake.
Here is my setup
import { Configuration, PopupRequest, PublicClientApplication } from '#azure/msal-browser'
export const msalConfig: Configuration = {
auth: {
clientId: process.env.NEXT_PUBLIC_MSAL_CLIENT_ID || '',
redirectUri: process.env.NEXT_PUBLIC_MSAL_REDIRECT_URL,
authority: `https://login.microsoftonline.com/${process.env.NEXT_PUBLIC_MSAL_TENANT}`,
navigateToLoginRequestUrl: true,
},
cache: {
cacheLocation: 'localStorage', // This configures where your cache will be stored
storeAuthStateInCookie: false,
},
}
export const loginRequest: PopupRequest = {
scopes: ['User.Read'],
}
export const msalInstance = new PublicClientApplication(msalConfig)
const currentAccounts = msalInstance.getAllAccounts()
console.log({ currentAccounts }) // returns empty array
This is how I warp my app with MsalProvider
import { ApolloProvider } from '#apollo/client'
import { MsalProvider } from '#azure/msal-react'
import { defaultClient } from 'apollo'
import { msalInstance } from 'msal-auth-config' // import msalInstance from config
import type { AppProps } from 'next/app'
import React from 'react'
const App = ({ Component, pageProps }: AppProps): JSX.Element => {
return (
<MsalProvider instance={msalInstance}>
<ApolloProvider client={defaultClient}>
<App />
</ApolloProvider>
</MsalProvider>
)
}
export default App
Here I want to return token
const authLink = setContext((_operation, { headers }) => {
const accounts = msalInstance.getAllAccounts()
//console.log({ accounts, headers })
if (accounts.length > 0) {
msalInstance.setActiveAccount(accounts[0])
}
return msalInstance
.acquireTokenSilent(loginRequest)
.then((response) => {
console.log(response) // return undefined
return { headers: { ...headers, Authorization: `Bearer ${response.idToken}` } }
})
.catch((error) => {
if (error instanceof InteractionRequiredAuthError) {
return msalInstance.acquireTokenRedirect(loginRequest)
}
return
})
})

Have you try to make it like this
import { useState, useEffect } from "react";
import { useMsal } from "#azure/msal-react";
import { InteractionStatus } from "#azure/msal-browser";
const { instance, accounts, inProgress } = useMsal();
const [loading, setLoading] = useState(false);
const [apiData, setApiData] = useState(null);
useEffect(() => {
if (!loading && inProgress === InteractionStatus.None && accounts.length > 0) {
if (apiData) {
// Skip data refresh if already set - adjust logic for your specific use case
return;
}
const tokenRequest = {
account: accounts[0], // This is an example - Select account based on your app's requirements
scopes: ["User.Read"]
}
// Acquire an access token
instance.acquireTokenSilent(tokenRequest).then((response) => {
// Call your API with the access token and return the data you need to save in state
callApi(response.accessToken).then((data) => {
setApiData(data);
setLoading(false);
});
}).catch(async (e) => {
// Catch interaction_required errors and call interactive method to resolve
if (e instanceof InteractionRequiredAuthError) {
await instance.acquireTokenRedirect(tokenRequest);
}
throw e;
});
}
}, [inProgress, accounts, instance, loading, apiData]);
if (loading || inProgress === InteractionStatus.Login) {
// Render loading component
} else if (apiData) {
// Render content that depends on data from your API
}
Read more here

you are probably missing the handleRedirectPromise...
once the redirect is done the promise should catch the account... if not try another aquireSilentToken to catch it in the promise below.
instance.handleRedirectPromise().then(resp => {
if (resp && resp.account) instance.setActiveAccount(resp.account);
});

Related

Stay logged in on refresh, JWT

On my website switching between pages is completely fine and works (it doesnt refresh or load due to redux) but the moment the page is refreshed or i manually enter a link to access, it logs me out. Also it just started happening now, yesterday when i was working on some other things in code, it never logged me out when I manually with links/urls navigated thru website or refreshing but now for some reason it doesnt work and I'm 99% sure I havent touched any auth part of the code...
This is my code:
authApiSlice:
import { apiSlice } from "../../app/api/apiSlice";
import { logOut, setCredentials } from "./authSlice";
export const authApiSlice = apiSlice.injectEndpoints({
endpoints: (builder) => ({
login: builder.mutation({
query: (credentials) => ({
url: "/auth",
method: "POST",
body: { ...credentials },
}),
}),
sendLogout: builder.mutation({
query: () => ({
url: "/auth/logout",
method: "POST",
}),
async onQueryStarted(arg, { dispatch, queryFulfilled }) {
try {
const { data } = await queryFulfilled;
console.log(data);
dispatch(logOut());
setTimeout(() => {
dispatch(apiSlice.util.resetApiState());
}, 1000);
} catch (err) {
console.log(err);
}
},
}),
refresh: builder.mutation({
query: () => ({
url: "/auth/refresh",
method: "GET",
}),
async onQueryStarted(arg, { dispatch, queryFulfilled }) {
try {
const { data } = await queryFulfilled;
console.log(data);
const { accessToken } = data;
dispatch(setCredentials({ accessToken }));
} catch (err) {
console.log(err);
}
},
}),
}),
});
export const { useLoginMutation, useSendLogoutMutation, useRefreshMutation } =
authApiSlice;
authSlice:
import { createSlice } from "#reduxjs/toolkit";
const authSlice = createSlice({
name: "auth",
initialState: { token: null },
reducers: {
setCredentials: (state, action) => {
const { accessToken } = action.payload;
state.token = accessToken;
},
logOut: (state, action) => {
state.token = null;
},
},
});
export const { setCredentials, logOut } = authSlice.actions;
export default authSlice.reducer;
export const selectCurrentToken = (state) => state.auth.token;
import { Outlet, Link } from "react-router-dom";
import { useEffect, useRef, useState } from "react";
import { useRefreshMutation } from "./authApiSlice";
import usePersist from "../../hooks/usePersist";
import { useSelector } from "react-redux";
import { selectCurrentToken } from "./authSlice";
const PersistLogin = () => {
const [persist] = usePersist();
const token = useSelector(selectCurrentToken);
const effectRan = useRef(false);
const [trueSuccess, setTrueSuccess] = useState(false);
const [refresh, { isUninitialized, isLoading, isSuccess, isError, error }] =
useRefreshMutation();
useEffect(() => {
if (effectRan.current === true || process.env.NODE_ENV !== "development") {
// React 18 Strict Mode
const verifyRefreshToken = async () => {
console.log("verifying refresh token");
try {
//const response =
await refresh();
//const { accessToken } = response.data
setTrueSuccess(true);
} catch (err) {
console.error(err);
}
};
if (!token && persist) verifyRefreshToken();
}
return () => (effectRan.current = true);
// eslint-disable-next-line
}, []);
let content;
if (!persist) {
// persist: no
console.log("no persist");
content = <Outlet />;
} else if (isLoading) {
//persist: yes, token: no
console.log("loading");
} else if (isError) {
//persist: yes, token: no
console.log("error");
content = (
<p className="errmsg">
{`${error?.data?.message} - `}
<Link to="/login">Please login again</Link>.
</p>
);
} else if (isSuccess && trueSuccess) {
//persist: yes, token: yes
console.log("success");
content = <Outlet />;
} else if (token && isUninitialized) {
//persist: yes, token: yes
console.log("token and uninit");
console.log(isUninitialized);
content = <Outlet />;
}
return content;
};
export default PersistLogin;
RequireAuth
import { useLocation, Navigate, Outlet } from "react-router-dom";
import useAuth from "../../hooks/useAuth";
const RequireAuth = ({ allowedRoles }) => {
const location = useLocation();
const { roles } = useAuth();
const content = roles.some((role) => allowedRoles.includes(role)) ? (
<Outlet />
) : (
<Navigate to="/prijava" state={{ from: location }} replace />
);
return content;
};
export default RequireAuth;
It just stopped worked for some reason, it shouldnt log me out when I refresh.

React Native - Authentication - trigger value to change Auth & UnAuth Stack Navigators

Here my App.js
import React, { useEffect } from "react";
import { NavigationContainer } from "#react-navigation/native";
import AuthStore from "./src/stores/AuthStore";
import AuthStackNavigator from "./src/navigation/AuthStackNavigator";
import UnAuthStackNavigator from "./src/navigation/UnAuthStackNavigator";
const App = () => {
useEffect(() => {
console.log("APP JS", AuthStore.userAuthenticated);
}, [AuthStore.userAuthenticated]);
return <NavigationContainer>
{AuthStore.userAuthenticated ? <AuthStackNavigator /> : <UnAuthStackNavigator />}
</NavigationContainer>;
};
export default App;
The AuthStore value of userAuthenticated is computed and updated on auto login or login.
Here AuthStore.js
import { userLogin, userRegister } from "../api/AuthAgent";
import { clearStorage, retrieveUserSession, storeUserSession } from "../utils/EncryptedStorage";
import { Alert } from "react-native";
import { computed, makeObservable, observable } from "mobx";
import { setBearerToken } from "../config/HttpClient";
class AuthStore {
user = {};
token = undefined;
refreshToken = undefined;
decodedToken = undefined;
constructor() {
makeObservable(this, {
token: observable,
refreshToken: observable,
user: observable,
decodedToken: observable,
userAuthenticated: computed,
});
this.autoLogin();
}
async doLogin(body) {
const resp = await userLogin(body);
console.log("AuthStore > userLogin > resp => ", resp);
if (resp.success) {
this.decodedToken = await this.getDecodedToken(resp.token);
this.setUserData(resp);
storeUserSession(resp);
} else {
Alert.alert(
"Wrong credentials!",
"Please, make sure that your email & password are correct",
);
}
}
async autoLogin() {
const user = await retrieveUserSession();
if (user) {
this.setUserData(user);
}
}
setUserData(data) {
this.user = data;
this.token = data.token;
setBearerToken(data.token);
}
get userAuthenticated() {
console.log('AuthStore > MOBX - COMPUTED userAuthenticated', this.user);
if (this.token) {
return true;
} else return false;
}
async logout() {
await clearStorage();
this.user = undefined;
this.token = undefined;
this.refreshToken = undefined;
this.decodedToken = undefined;
}
}
export default new AuthStore();
The main problem is that the AuthStore.userAuthenticated value even when it changes on AuthStore it does not triggered by useEffect of the App.js.
So, when I log in or log out I have to reload the App to trigger the useEffect hook and then the navigators are only updated.
You can use useMemo hook to achive this.
const App = () => {
const [userToken, setUserToken] = useState("")
const authContext: any = useMemo(() => {
return {
signIn: (data: any) => {
AsyncStorage.setValue("token", data.accessToken);
setUserToken(data.accessToken);
},
signOut: () => {
setUserToken("");
AsyncStorage.setValue("token", "");
},
};
}, []);
return (
<AuthContext.Provider value={authContext}>
{userToken.length ? (
<UnAuthStackNavigator />
) : (
<AuthStackNavigator />
)}
)
</AuthContext.Provider>
)
}
AuthContext.ts
import React from "react";
export const AuthContext: any = React.createContext({
signIn: (res: any) => {},
signOut: () => {},
});
Now you can use this functions in any files like this:
export const SignIn = () => {
const { signIn } = useContext(AuthContext);
return (
<Button onPress={() => {signIn()}} />
)
}
If your primary purpose is to navigate in and out of stack if authentication is available or not then asynstorage is the best option you have to first
store token.
const storeToken = async (value) => {
try {
await AsynStorage.setItem("userAuthenticated", JSON.stringify(value));
} catch (error) {
console.log(error);
}
};
("userAuthenticated" is the key where we get the value )
Now go to the screen where you want this token to run turnery condition
const [token, setToken] = useState();
const getToken = async () => {
try {
const userData = JSON.parse(await AsynStorage.getItem("userAuthenticated"))
setToken(userData)
} catch (error) {
console.log(error);
}
};
Now use the token in the state then run the condition:
{token? <AuthStackNavigator /> : <UnAuthStackNavigator />}
or
{token != null? <AuthStackNavigator /> : <UnAuthStackNavigator />}

React MSAL not working after page refresh

I am using #azure/msal-react and #azure/msal-browser for authentication in our application. So far i have intialized a msal instance and used it to acquire a token and fetch the alias. Everything works until i do a page refresh i want to understand if this is a cache issue i see data is present in local storage regarding msal but still getting null in active account.
here you can see the msal instance with configuration (authProvider)
AuthProvider.js
import * as APIConstants from "./Common/APIConstants";
import {
BrowserCacheLocation,
PublicClientApplication,
LogLevel
} from '#azure/msal-browser';
const loggerCallback = (logLevel, message) => {
console.log(message);
};
const configuration = {
auth: {
authority: APIConstants.TENANT,
clientId: APIConstants.CLIENT_ID,
postLogoutRedirectUri: window.location.origin,
redirectUri: window.location.origin,
validateAuthority: true,
navigateToLoginRequestUrl: true,
},
cache: {
cacheLocation: BrowserCacheLocation.localStorage,
storeAuthStateInCookie: true,
},
system: {
loggerOptions: {
loggerCallback,
logLevel: LogLevel.Info,
piiLoggingEnabled: false
}
}
};
export const authProvider = new PublicClientApplication(configuration);
Login.js
import React, { Component } from "react";
import { authProvider } from "../../authProvider";
import * as APIConstants from "../../Common/APIConstants";
import {
EventType,
} from '#azure/msal-browser';
class Login extends Component {
constructor() {
super()
this.successEventId = null;
}
loginSuccessCallback = (event) => {
if (event.eventType === EventType.LOGIN_SUCCESS && event.payload) {
const payload = event.payload;
const account = payload.account;
authProvider.setActiveAccount(account);
this.props.history.push("/dashboard");
}
}
componentDidMount = () => {
this.successEventId = authProvider.addEventCallback(this.loginSuccessCallback);
}
componentWillUnmount = () => {
if (this.successEventId)
authProvider.removeEventCallback(this.successEventId);
}
onLDAPLogin = () => {
authProvider.loginRedirect({
scopes: [APIConstants.RESOURCE_URI],
})
}
render() {
return (
< div >
<button onClick={() => this.onLDAPLogin()}>LDAP Login</button>
</div>
);
}
}
export default Login;
dashboard.js
purpose dashboard.js is to collect data through api call where you use alias and token of that user to make the call
const getAllData = async () => {
let user = await Services.getLogedInUser();
if (user) {
let username = user.username;
let alias = username.split("#")[0];
let token = await Services.getAccessToken();
if (token) {
await initiateApiCall(alias,token);
} else {
props.history.push("/");
}
}
else {
props.history.push("/");
}
}
Services.js
export const getAccessToken = async () => {
const activeAccount = authProvider.getActiveAccount();
const accounts = authProvider.getAllAccounts();
if (!activeAccount && accounts.length === 0) {
/*
* User is not signed in. Throw error or wait for user to login.
* Do not attempt to log a user in outside of the context of MsalProvider
*/
}
const request = {
scopes: [APIConstants.RESOURCE_URI],
account: activeAccount || accounts[0]
};
const authResult = await authProvider.acquireTokenSilent(request);
console.log('auth result acquire token ', authResult);
return authResult.accessToken
};
export const getLogedInUser = async () => {
console.log('authProvider', authProvider);
console.log('all accounts ', authProvider.getAllAccounts());
let account = authProvider.getActiveAccount();
console.log('account ', account);
return account;
};
As it is shown in the console i get both active account and all accounts value but in case i refresh the page active account is null and all accounts is an empty array

How to properly store a JWT in a cookie in react using NextJS with typescript and the useContext hook

The user is set and persists on all the pages but when the browser window is refreshed the user is logged out. Not Sure if the problem is how I'm setting the cookies, any advice or an alternative is very welcomed.
Here the UserContext file:
import { destroyCookie } from "nookies";
import { setCookie } from "nookies";
import React from "react";
// import { useCookies } from "react-cookie"
import cookie from "cookie";
import { Person, tokenAuth } from "../auth";
import { LoginFormValues } from "../components/Auth/LoginForm";
import { noop } from "../helpers";
import { COOKIE_AUTH_NAME, MONTH } from "../helpers/user";
interface UserContextProps {
user?: User;
logout: () => void;
login: (values: LoginFormValues) => Promise<void>;
}
const UserContext = React.createContext<UserContextProps>({
user: undefined,
logout: noop,
login: async () => noop(),
});
export const UseUserContext = () => React.useContext(UserContext);
interface UserContextProviderProps {
children: React.ReactNode;
user?: User;
}
export const UserContextProvider = ({
children,
user: initialUser,
}: UserContextProviderProps) => {
const [user, setUser] = React.useState<User | undefined>(initialUser);
const login = async (values: LoginFormValues) => {
try {
const tokenResponse = await tokenAuth(values);
if (tokenResponse.status === 200) {
const { token } = tokenResponse.data;
setCookie(null, COOKIE_AUTH_NAME, token, {
maxAge: 3600,
path: "/",
sameSite: "strict",
});
const userResponse = await Person();
if (userResponse.status === 200) {
setUser(userResponse.data);
}
}
} catch (error) {
let errorMessage = "failed";
if (error instanceof Error) {
errorMessage = error.message;
}
console.error(errorMessage);
}
};
const logout = () => {
setUser(undefined);
destroyCookie(null, COOKIE_AUTH_NAME);
};
return (
<UserContext.Provider value={{ user, login, logout }}>
{children}
</UserContext.Provider>
);
};
export default UserContext;
Im wondering if my problem is in my _app.tsx Im also using a Django rest api to retrieve the data this how I'm calling the api. This is what the file looks like:
import axios, { AxiosResponse } from "axios";
import { parseCookies } from "nookies";
export function getJwtToken() {
return sessionStorage.getItem("jwt");
}
async function request<T>(
method: "GET" | "POST" | "PATCH" | "DELETE" | "PUT" | "OPTIONS",
url: string,
data?: Record<string, unknown>
): Promise<AxiosResponse<T>> {
const cookies = parseCookies();
const { authToken } = cookies;
const baseUrl = process.env.NEXT_PUBLIC_API_URL;
const response: AxiosResponse = await axios({
method,
url: `${baseUrl}${url}`,
data,
withCredentials: true,
headers: {
"Content-Type": "application/json",
},
});
axios.defaults.headers.common["Authorization"] = authToken;
return response;
}
interface TokenData {
token: string;
}
export function tokenAuth({ email = "", password = "" }) {
return request<TokenData>("POST", "/api/client/login", {
email,
password,
});
}
export function Person() {
return request<User>("GET", `/api/client/user`);
}
This file is pretty basic and I don't think its the route of the problem.

React Hook and Context not persisting on redirect

I am still navigating React hooks and contexts and have been attempting to use both to store user information for further usage in other portions of my app after successful authentication from an axios request. With my code that follows, I successfully set the state used in the context, but when the state is accessed following a redirect that occurs directly after setting the value, it comes back as undefined and I'm not sure what is preventing the value from being stored.
Provided is my context and hook (AppSession.js):
import React, { createContext, useContext, useState } from 'react'
export const SessionContext = createContext(null);
const AppSession = ({ children }) => {
const [user, setUser] = useState()
if (user){
console.log("useState: Authenticated")
console.log(user)
} else {
console.log("useState: Not authenticated")
console.log(user)
}
return (
<SessionContext.Provider value={{user, setUser}}>
{children}
</SessionContext.Provider>
)
}
export const getUserState = () => {
const { user } = useContext(SessionContext)
return user;
}
export const updateUserState = () => {
const { setUser } = useContext(SessionContext)
return (user) => {
setUser(user);
}
}
export default AppSession;
**Provided is the axios request and console logs upon successful response (login.js):**
axios.post(
'/api/auth/signin/',
{ email, password },
{
headers: {
'Content-Type': 'application/json'
},
withCredentials: true
}).then((res) => {
console.log(res.data) // {authenticated: true, user_id: "071c7b80-6b4d-462c-8c4a-4fa613a7e8b6", user_email: "Alysson_Runolfsdottir#yahoo.com"}
const data = res.data; //
console.log("updateUserState")
setUser(data)
}).then(()=> {
return window.location = '/app/profile/'
}).catch((err) => {
console.log(err)
})
// Console.logs
{ authenticated: true, user_id: "071c7b80-6b4d-462c-8c4a-4fa613a7e8b6", user_email: "Alysson_Runolfsdottir#yahoo.com" } // login.js
updateUserState // login.js
useState: Authenticated // AppSession.js
{ authenticated: true, user_id: "071c7b80-6b4d-462c-8c4a-4fa613a7e8b6", user_email: "Alysson_Runolfsdottir#yahoo.com" } // AppSession.js
Then the code for profile.js which is the result of redirect to /app/profile with console logs:
import React from 'react'
import { getUserState } from '../../contexts/AppSession'
import Layout from '../../components/Universal/Layout'
export default function Profile(props) {
const checkUser = getUserState()
console.log(checkUser)
console.log(props)
return (
<Layout
title="Signin"
description="TEST"
>
<h1>Protected Page</h1>
<p>You can view this page because you are signed in.</p>
<br />
<b>Check User: {checkUser}</b>
</Layout>
)
}
// Console.logs
useState: Not authenticated // AppSession.js
undefined // AppSession.js (console.log(user))
undefied // profile.js (console.log(checkUser))
As you can see the storage is short-lives as the subsuquent page that loads upon redirect access the user state and it is undefined. Any idea why this might be?

Resources