How to re-route a React-Admin url - reactjs

I'm trying to configure the login page for my React-Admin app. I'm using authProvider and LoginPage components, as per the documentation and my login process is generally working.
I have the app running locally on port 3000. But when I go to http://localhost:3000, I'm automatically redirected to http://localhost:3000/#/login.
What specifically is driving that redirection? I don't specify that .../#/login url within the app itself.
I'm using an old version of React-Admin (2.9), which I understand uses Redux. Is that redirection to .../#/login a function of Redux? Or of React-Admin itself?
My understanding is I can maybe use HashHistory or BrowserHistory to prevent the # - but not sure if that's compatible with React-Admin.
The actual issue I'm having is that once I deploy the app to my domain, the login process behaves differently compared to when I run on localhost - which is making pre-deployment testing difficult.
That is, http://localhost:3000 and http://localhost:3000/#/login both allow me to login successfully. But when I deploy to my domain, http://www.example.com allows me to login, while http://www.example.com/#/login does not.
Any idea why this would be? And can I configure a React-Admin app to not re-route to http://www.example.com/#/login?

"If the promise is rejected, react-admin redirects by default to the /login page.
You can override where to redirect the user in checkAuth(), by rejecting an object with a redirectTo property:"
React-admuin 2.9:
https://marmelab.com/react-admin/doc/2.9/Authentication.html#checking-credentials-during-navigation
// in src/authProvider.js (React-admin 2.9)
import { AUTH_LOGIN, AUTH_LOGOUT, AUTH_ERROR, AUTH_CHECK } from 'react-admin';
export default (type, params) => {
if (type === AUTH_LOGIN) {
// ...
}
if (type === AUTH_LOGOUT) {
// ...
}
if (type === AUTH_ERROR) {
// ...
}
if (type === AUTH_CHECK) {
return localStorage.getItem('token') ? Promise.resolve() : Promise.reject({ redirectTo: '/no-access' });
}
return Promise.reject('Unknown method');
};
React-admin 4.3: https://marmelab.com/react-admin/AuthProviderWriting.html
// in src/authProvider.js
export default {
login: ({ username, password }) => { /* ... */ },
checkError: (error) => { /* ... */ },
checkAuth: () => localStorage.getItem('auth')
? Promise.resolve()
: Promise.reject({ redirectTo: '/no-access' }),
// ...
}

Related

Logout all tabs with msal-react when using localStorage

I have a React 18.x with NextJS 12.x application that uses msal-react 1.4.4 (relies on msal-browser 2.28.0) and Azure B2C for authentication. My config is like this:
export const msalConfig: Configuration = {
auth: {
clientId: clientId as string,
authority: `https://${tenant}.b2clogin.com/${tenant}.onmicrosoft.com/${b2cPolicy}`,
knownAuthorities: [`${tenant}.b2clogin.com`],
redirectUri: '/openid-redirect',
postLogoutRedirectUri: '/',
},
cache: {
cacheLocation: 'localStorage',
},
};
Pages are protected by a <MsalAuthenticationTemplate interactionType={InteractionType.Redirect} ...> component to enforce login via the redirect flow. This all works fine.
In my navigration bar I have a logout button:
const handleLogout = () => {
msalInstance.logoutRedirect({
account: msalInstance.getActiveAccount(),
});
};
<button onClick={() => handleLogout()}>
Logout
</button>
And the tab itself gets logged out just fine.
All other tabs loose access to the access_token and other information so they are effectively "logged out" and cannot call API's with bearer authentication anymore. However, the UI of that application does not show the user as logged out.
I had expected all other tabs to notice the logout and update accordingly, possibly redirecting users to another page. Or at the least I would've expected the loggerCallback I've configured to show there's an event or notification that some other tab has logged us out.
If I manually do window.addEventListener('storage', evt => console.log(evt)); I do see the other tabs notice storage is being cleared.
I found another related question which is about cross-device logout, which I expect to rely on the OpenID Session Management spec. I guess that solution could work for me, but the other question nor answer contain a working solution for that either.
The relevant MSDN documentation doesn't mention anything about "multiple tabs" or something similar.
How can I configure my application and msal-react to notice sign outs from other tabs?
Workaround
For now we've used the following workaround:
export function useLogoutInOtherTabsListener() {
const router = useRouter();
useEffect(() => {
const handler = (evt: StorageEvent) => {
if (evt.key === 'logout-event' && evt.newValue === 'started') {
msalInstance.logoutRedirect({
onRedirectNavigate: () => false, // No need to do redirects via msal, we'll just route the user to '/' ourselves
});
router.push('/');
}
};
window.addEventListener('storage', handler);
return () => {
window.removeEventListener('storage', handler);
};
}, [router]);
}
export function logoutAllTabs(): void {
// We'd prefer to use an msal-mechanism, but so far couldn't find any.
// See also: https://stackoverflow.com/q/73051848/419956
window.localStorage.setItem('logout-event', 'started');
window.localStorage.removeItem('logout-event');
msalInstance.logoutRedirect();
}
And call useLogoutInOtherTabsListener() in _app.tsx.
Clearing the session ids should be able to log out of all the pages. To do this we can use the concept of the front channel logout .
In front channel logout we basically load a different page which will do all the logging out process and clear cache and stop local access to the site. Here the page will be loaded in a hidden iframe and perform only the sign-out operation.
But for this to work we have to set system.allowRedirectInIframe to true. Also we have to register logout url in the portal.
const msal = new PublicClientApplication({
auth: {
clientId: "my-client-id"
},
system: {
allowRedirectInIframe: true
}
})
// Automatically on page load
msal.logoutRedirect({
onRedirectNavigate: () => {
// Return false to stop navigation after local logout
return false;
}
});
Refer this following documentation the above code is from there.

Directly redirect to AAD login page on hitting the URL in browser

Using MSAL 2.0 in React we can easily achieve redirect to login page using below methods
Login with pop-up
Login with redirect
But both the methods can be performed on a login/signin button action.
In my case, I want to redirect my React app directly to login page, when the URL is hit in the browser i.e. without button action.
Also, while doing the redirect I also need to pass my multiple scopes, for user consent.
I did some research but didn't found any feasible solutions.
Please can anyone help me with this ?
When an application starts it always start with the app.js. In the app.js class constructor you can check your token if it is valid/invalid and or if expired.
In order to redirect your React app directly to login page try this:
state = {
show: false;
}
componentWillReceiveProps(nextProps) {
if (nextProps.children !== this.props.children) {
this.checkAuth();
}
}
componentDidMount() {
this.checkAuth();
}
checkAuth() {
Api.checkAuth()
.then(result => {
if (result.success) {
this.setState({ show: true });
} else {
return <Redirect to='/login' />
}
});
}

React Next js app redirect to login is premature

After a lot of searching for several hours, I have the following code to redirect from a user profile page if not logged in.
NOTE: Simply showing a not authorized page is easy but its the redirect thats messing things up.
The code does the job of redirecting when user is not logged in.
const Dashboard = () => {
const [user, { mutate }] = useCurrentUser();
const router = useRouter();
useEffect(() => {
// redirect to login if user is not authenticated
if (!user) router.push('/login');
}, [user]);
...
The problem is when a user is logged in and directly goes to /user/dashboard route, for a split second, user is undefined may be so it redirects to login. When it gets to login, it finds that user is authenticated so redirects to home page because I am redirecting a logged in user to home page.
How to prevent that split second of "not a user" status when page is first loading?
I tried -
getInitialProps
getServerSideProps - Cant use router because next router can only be used on client side
componentDidMount - UseEffectI tried above is the equivalent correct?
Edit: Based on answer below, I tried this but still directly takes user to login first. I am using react cookies and I do see loggedIn cookie as true when user is logged in and its not set when user is not logged in.
Dashboard.getInitialProps = ({ req, res }) => {
console.log(req.headers.cookie)
var get_cookies = function(request) {
var cookies = {};
request.headers && request.headers.cookie.split(';').forEach(function(cookie) {
var parts = cookie.match(/(.*?)=(.*)$/)
cookies[ parts[1].trim() ] = (parts[2] || '').trim();
});
return cookies;
};
//console.log(get_cookies(req)['loggedIn']);
if (get_cookies(req)['loggedIn'] == true) {
console.log("entered logged in")
return {loggedIn: true};
}
else {
console.log("entered not logged in")// can see this on server console log
// User is not logged in, redirect.
if (res) {
// We're on the server.
res.writeHead(301, { Location: '/login' });
res.end();
} else {
// We're on the client.
Router.push('/login');
}
}
}
You can implement redirect when not authenticated in getServerSideProps
Below example is based on JWT Authentication with cookies.
export const getServerSideProps = async (ctx) => {
const cookie = ctx.req.headers.cookie;
const config = {
headers: {
cookie: cookie ?? null
}
}
let res;
try {
// your isAuthenticated check
const res = await axios('url', config);
return { props: { user: res.data } };
} catch (err) {
console.error(err);
ctx.res.writeHead(302, {
Location: 'redirectUrl'
})
ctx.res.end();
return;
return { props: { user: null } };
}
}
You should be able to use getInitialProps to redirect. You just need to check whether you're on the server or the client and use the proper redirect method. You can't use hooks in getInitialProps so your useCurrentUser approach won't work and you'll need some other way to check whether the user is authed. I don't know anything about the structure of your application, but it's probably just some kind of request to wherever you're storing the session.
import Router from 'next/router';
const Dashboard = (props) => {
// props.user is guaranteed to be available here...
};
Dashboard.getInitialProps = async ({ res }) => {
// Check authentication.
// Await the response so that the redirect doesn't happen prematurely.
const user = await ...
// User is logged in, return the data you need for the page.
if (user) {
return { user };
}
// User is not logged in, redirect.
if (res) {
// We're on the server.
// Make the redirect temporary so it doesn't get cached.
res.writeHead(307, { Location: '/login' });
res.end();
} else {
// We're on the client.
Router.push('/login');
}
};
After many hours of struggle, there was one number that was breaking this.
Instead of
res.writeHead(301, { Location: '/login' });
I used
res.writeHead(307, { Location: '/login' });
and it worked.
301 is a permanent redirect so if we use that, when the user logs in, the browser still holds the redirect cache.
From next js docs
Next.js allows you to specify whether the redirect is permanent or not with the permanent field. This is required unless you need to specify the statusCode manually
When permanent is set to true we use a status code of 308 and also set a Refresh header for backwards compatibility with IE11.
When permanent is set to false we use a status code of 307 which is not cached by browsers and signifies the redirect is temporary.
Next.js permits the following status codes:
-301 Moved `Permanently`
-302 Found
-303 See Other
-307 `Temporary` Redirect
-308 Permanent Redirect

React: Redirect to new page from FUNCTION not from COMPONENT (working with React Router)

I have a function running using a setInterval timer. Its purpose is to ask the server if the login/sessionID I have in local storage is still valid. If the session has expired, I want to give an alert and route to the Login page.
I would normally use something like this.props.history.push("/login") to accomplish the redirect. But this cannot be a Component; being run by setInterval, it must be a function. The React-Router history.push option is not available outside of Components, AFAIK. What would be my best alternative?
Here is the setInterval code that happens at Login:
sessionStorage.setItem('mycha_sessionID',returnData.data.sessionID)
let intervalID = setInterval(CheckSession,1*30*1000)
sessionStorage.setItem('intervalID',intervalID)
And the function code that will run every interval:
import {axiosGet} from "./AxiosCalls";
import {ClearSession} from "./ClearSession";
export function CheckSession() {
// Call Wordpress to see if the current session ID is still valid
if (sessionStorage.getItem('mycha_sessionID')) {
axiosGet(sessionStorage.getItem('mycha_base_url')+"/wp-json/mycha/check-session")
.then(res => {
if ((false === res.data) || ("" === res.data)) {
ClearSession()
alert("Your login session has expired. Please log in again before continuing.")
// Redirect to Login here
}
})
.catch(err => alert(err));
}
return
}
I may be misunderstanding, but why not just call
window.location.href = 'https://www.my-site.com/login'

Redirecting from server side in NextJS

I'm developing a Next.JS app where a user should login to see the content.I want to redirect the user to '/' if their username and password are correct.However my implementation seems not working.
I searched on SO for questions regarding this,but all of them were talking about redirecting with getInitialProps but it doesn't help me since I want to redirect user from my custom express server.
Login.js
async handleSubmit(event) {
event.preventDefault()
const { username, password } = this.state
try {
const response = await fetch('/log_in', {
method: 'post',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password })
})
}
catch (error) {
console.log(error)
}
}
server.js
app.post('/log_in', (req,res) => {
console.log(`login form data`);
console.log(`Username : ${req.body.username}`);
console.log(`password : ${req.body.password}`);
if(req.body.username == "user" && req.body.password == "123"){
res.redirect('/')
}
})
This is now possible on Next.js 10+ without having to do any sort of response header manipulation directly. Use getServerSideProps or getStaticProps and return a redirect key as the only value from it if your conditions are met:
export async function getServerSideProps(context) {
if(req.body.username == "user" && req.body.password == "123"){
return {
redirect: {
permanent: false,
destination: "/"
}
}
}
}
It's a good idea for SEO to redirect server side if you can (not for login like the questions mention).
But for things like user language etc it's probably a good idea to use server side redirects like this.
----- REMOVED since it was bad code --------
Update, I ended up with this as I did have some problems with headers already sent. Please let me know how this can be fixed as it's not the most ideal (although Google does understand it according to this article https://www.seroundtable.com/google-meta-refresh-redirects-work-25335.html)
export default function HomePage({ language }) {
return (
<Head>
<title>-</title>
<meta httpEquiv="refresh" content={`0;url=/${language}/discover/0`} />
</Head>
);
}
Update
Nextjs is adding native support there is an RFC available: https://github.com/vercel/next.js/discussions/17078
With Next.JS, you need to redirect the client on the client side. Try to return a 200 status code if the login is successful and then use the client router on success to navigate to another page. This example should give you an idea on how you can programmatically push a new page to the router history:
import Router from 'next/router'
() => {
if(code==200){
Router.push('/')
}
}

Resources