I'm working on an app with a login page and the rest of the pages of the app (should be logged in to view). I'm using react-boilerplate. From this example, I edited my asyncInjectors.js file to have redirectToLogin and redirectToDashboard methods:
//asyncInjectors.js
export function redirectToLogin(store) {
return (nextState, replaceState) => {
const isAuthenticated = store.getState().get('app').get('isAuthenticated');
if (!isAuthenticated) {
replaceState({
pathname: '/login',
state: {
nextPathname: nextState.location.pathname,
},
});
}
};
}
export function redirectToDashboard(store) {
return (nextState, replaceState) => {
const isAuthenticated = store.getState().get('app').get('isAuthenticated');
if (isAuthenticated) {
replaceState('/');
}
}
}
Then I just set the redirectToLogin as the onEnter of the pages and redirectToDashboard for the login page.
It works fine but when the page is refreshed (F5) when logged in, the login page renders briefly and then renders the actual page. The login page just dispatches an authenticate action in componentWillMount and then redirects in componentDidUpdate:
//login.js
componentWillMount() {
this.props.dispatch(authenticate());
}
componentDidUpdate(prevProps, prevState) {
if (this.props.isAuthenticated) {
const nextPathname = prevProps.location.state ? prevProps.location.state.nextPathname : '/';
browserHistory.push(nextPathname);
}
}
The container for the pages also has the same componentWillMount code. Not sure if it's because of the sagas but here's the code:
//sagas.js
export function* login({ user, password }) {
try {
const token = yield call(app.authenticate, {
strategy: 'local',
user,
password,
});
return token;
} catch (error) {
return onError(error);
}
}
// For page refresh after logging in
export function* authenticate() {
try {
const token = yield call(app.authenticate);
return token;
} catch (error) {
return onError(error);
}
}
export function* logout() {
try {
const response = yield call(app.logout);
return response;
} catch (error) {
return onError(error);
}
}
export function* loginFlow() {
while (true) {
const request = yield take(LOGIN_REQUEST);
const winner = yield race({
auth: call(login, request.data),
logout: take(LOGOUT_REQUEST),
});
if (winner.auth && winner.auth.accessToken) {
yield put(actions.setAuthState(true));
}
}
}
export function* logoutFlow() {
while (true) {
yield take(LOGOUT_REQUEST);
yield put(actions.setAuthState(false));
yield call(logout);
browserHistory.push('/login');
}
}
export function* authenticateFlow() {
while (true) {
yield take(AUTHENTICATE);
const response = yield call(authenticate);
if (response && response.accessToken) {
yield put(actions.setAuthState(true));
}
}
}
export default [
loginFlow,
logoutFlow,
authenticateFlow,
];
How do I get rid of the flashing login page?
EDIT:
When I tried gouroujo's answer, I couldn't logout.
//asyncInjectors.js
import jwtDecode from 'jwt-decode';
export function redirectToLogin(store) {
return (nextState, replaceState, callback) => {
const token = localStorage.token;
if (token) {
const jwt = jwtDecode(token);
if (jwt.exp <= (new Date().getTime() / 1000)) {
store.dispatch(actions.setAuthState(false));
replaceState({
pathname: '/login',
state: {
nextPathname: nextState.location.pathname,
},
});
}
}
store.dispatch(actions.setAuthState(true));
callback();
};
}
When I hit refresh, the login page doesn't show but now I can't log out.
You have two way to avoid flashing the login page on initial render : make your authenticate function synced or wait the answer with a loading page.
1- Check if your token is present and valid (expiration date) client-side to choose if you have to redirect the user to the login or the dashboard page first. When the answer come back from your server you correct your initial guess but in the vaste majority you won't need to.
user landing ->
check the token client-side -> redirect to login if needed
check the token server-side -> wait answer -> re-redirect if needed
To check the token client-side you have to check the local storage. For example:
class App extends Component {
requireAuth = (nextState, replace) => {
if (!localStorage.token) {
replace({
pathname: '/login',
state: { nextPathname: nextState.location.pathname }
})
}
}
render() {
return (
<Router history={browserHistory}>
<Route path="/login" component={LoginPage} />
<Route
path="/"
component={AppLayout}
onEnter={this.requireAuth}
> ... </Route>
)
}
}
If you use a token with a relatively short expiration date, you will also have to check the expiration date, thus decode the token.
try {
const jwt = JWTdecode(token);
if (moment().isBefore(moment.unix(jwt.exp))) {
return nextState;
} else {
replace({
pathname: '/login',
state: { nextPathname: nextState.location.pathname }
})
}
} catch (e) {
replace({
pathname: '/login',
state: { nextPathname: nextState.location.pathname }
})
}
2- Show a loading screen before you receive the answer from the server. add some Css transition effect on opacity to avoid the "flash"
Related
I am using NextAuth for auth
const options = {
providers: [
EmailProvider({...}),
],
pages: {
signIn: SIGN_IN_URL,
verifyRequest: AUTH_URL,
},
callbacks: {
async session(session) {
return { ...session }
},
async signIn({ user }) {
const result = ...
if (result) {
return true
} else {
return false
}
},
},
}
I have a protected page /dashboard
export const getServerSideProps = async (context) => {
const session = await getSession(context)
if (!session) {
return {
redirect: {
permanent: false,
destination: SIGN_IN_URL,
},
}
}
return {
props: {},
}
}
If an unauthorised user tries to access the page they get redirected to the sign in page http://localhost:3000/auth/signin
After successful login it redirects back to /.
But how do I set NextAuth up to redirect back to the originating page (in this case /dashboard) after successful log in?
Specify a callbackUrl in the query string. It should be a url encoded path like %2Fdashboard, for example in your snippet:
export const getServerSideProps = async (context) => {
const session = await getSession(context)
if (!session) {
return {
redirect: {
permanent: false,
destination: "/api/auth/signin?callbackUrl=%2Fdashboard",
},
}
}
return {
props: {},
}
}
Going to /api/auth/signin will use the custom signin page you specified in your nextauth config. To see this working in production, check out the official nextauth example.
I wanna make simple protected route.
I have credentials provider and nextAuth middleware. I just wanna make simple logic:
if user is logged in he can visit /profile, and if he visits /signup or /signin redirect him to /profile, and if he isnt logged he cant visit /profile and redirect him to /signin
some routes are neutral - for example he can visit /shop while being logged in or not.
there is my [...nextauth].ts
export default NextAuth({
session: {
strategy: 'jwt',
},
providers: [
CredentialsProvider({
type: 'credentials',
async authorize(credentails) {
const { password, email } = credentails as Signin
try {
const client = await connectToDatabase()
if (!client) return
const db = client.db()
const user = await existingUser(email, db)
if (!user) throw new Error('Invalid credentails!')
const isPasswordCorrect = await verifyPassword(password, user.password)
if (!isPasswordCorrect) throw new Error('Invalid credentails!')
return { email: user.email, name: user.name, id: user._id.toString() }
} catch (e: unknown) {
if (e instanceof Error) {
throw new Error(e.message)
}
}
},
}),
],
})
Apart from other answers what you can do is-
At component mount at signin and sign up check user is authenticated or not. If authenticated. use router.push to profile else be at signin/signup.
At profile again check for authentiction at component mount, if not auth push to signin else be at profile. Important thing here is don't show the layout, content of profile page before checking user is authenticated or not. Use a spiner or loader till auth check is going on.
write a middleware
const authorizedRoles = (...roles) => {
return (req, res, next) => {
if (!roles.includes(req.user.role)) {
return next(
// write logic to handle errors
new ErrorHandler(
`Role (${req.user.role}) is not allowed`,
403
)
);
}
next();
};
};
then whichever routes you want to protect, use this middleware. Then on protected pages' getServerSideProps
export async function getServerSideProps(context) {
const session = await getSession({ req: context.req });
if (!session || session.user.role !== "admin") {
return {
redirect: {
destination: "/home",
// permanent - if `true` will use the 308 status code which instructs clients/search engines to cache the redirect forever.
permanent: false,
},
};
}
return {
props: {},
};
}
My React-Redux app is using JWTs to handle authentication like so:
import axios from 'axios';
import { getLocalAccessToken, getLocalRefreshToken, updateLocalTokens } from './token-service';
async function attachAccessToken(reqConfig) {
const accessToken = getLocalAccessToken();
if (accessToken) reqConfig.headers.authorization = `Bearer ${accessToken}`;
return reqConfig;
}
export default (opts) => {
const API = axios.create(opts);
API.interceptors.request.use(attachAccessToken);
API.interceptors.response.use(
(res) => res.data,
async (err) => {
const { status, config: reqConfig, data: message } = err.response;
const isAuthTokenErr = !reqConfig.url.includes('/login') && status === 401 && !reqConfig._isRetried;
if (!isAuthTokenErr) return Promise.reject(message);
else {
reqConfig._isRetried = true;
try {
const { data: newTokens } = await axios.post('/auth/reauthorize', { refreshToken: getLocalRefreshToken() });
updateLocalTokens(newTokens);
return API(reqConfig); //retry initial request with new access token
} catch (reauthErr) {
const message = reauthErr.response.data;
return Promise.reject('Session expired. Please sign-in again');
}
}
}
);
return API;
};
Clients are given access to protected routes (views) as long as there is a user in the Redux store i.e.
import { Redirect, Route } from 'react-router-dom';
import { useSelector } from 'react-redux';
export default function ProtectedRoute({ component: Component, ...rest }) {
const isLoggedIn = useSelector(state => state.auth.user);
return (
<Route
{...rest}
render={(routeProps) => {
if (isLoggedIn) return <Component />;
else return <Redirect to="/auth/login" />;
}}
/>
);
}
When the refresh token expires, the client will no longer be able to access protected endpoints on the backend as desired.
That being said, on the frontend, the client remains logged in and able to access protected routes (views), albeit with no data loading.
My question is, for any given 401 unauthenticated response (apart from bad login credentials), how can I use the reauthErr to either:
a) Log the client out automatically i.e. clear the client's JWTs in LocalStorage as well as remove the user from the Redux store?
Or
b) more elegantly, prompt the client with a modal, for example, that their session has expired and that they will need to login again?
Thank you!
I'm trying to use Redirect from react-router-dom.
So after making an API post request, on the success I want to move back to the homepage.
Any ideas what's wrong with the following:
try {
const resp = await axios.post('http://localhost:8080/add', page);
if (resp.status == 200) {
return <Redirect to="/" />
}
} catch (err) {
console.log(err);
}
Ok, I read that the Redirect has to be in render.
So in my functional component, I've added a boolean for redirect which I update once the POST request has been successful.
However this doesn't seem to be valid:
if (redirect) {
return <Redirect to="/" />
}
As I get the error: Error: Invariant failed: You should not use <Redirect> outside a <Router>
Thanks.
You may have to use setState to make the state change and Redirect in render
class MyComponent extends React.Component {
state = {
redirect: false
}
async foo () {
try {
const resp = await axios.post('http://localhost:8080/add', page);
if (resp.status == 200) {
this.setState({redirect:true})
}
} catch (err) {
console.log(err);
}
}
render () {
const { redirect } = this.state;
if (redirect) {
return <Redirect to='/'/>;
}
...
}
I have a backend API to which I send email and password. In return, it provides an auth token after successful authentication. I have written the code for sending this API request in a file auth.js. It looks like this:
import axios from "axios";
export const auth = {
isAuthenticated: false,
login(user) {
const config = {
headers: {
"Content-Type": "application/json"
}
};
const body = JSON.stringify({ email: user.email, password: user.password });
return axios
.post("http://localhost:5000/userauth/login", body, config)
.then(res => {
localStorage.setItem("token", res.data.token);
this.isAuthenticated = true;
return res.data;
})
.catch(err => {
this.isAuthenticated = false;
console.log(err);
});
}
};
I am calling auth in App.js. Inside this I file I have a private route '/dashboard' which can be accessed only after authentication. If not authenticated, it redirects to '/login' route.
Here is the code for it:
import { auth } from "./actions/auth";
// rest of imports ...
export default function App() {
return (
<Router>
<Route path="/" exact component={Home} />
<Route path="/login" component={Login} />
<PrivateRoute path="/dashboard" component={Dashboard} />
</Router>
);
}
// ... ...
const PrivateRoute = ({ component: Component, ...rest }) => (
<Route
{...rest}
render={props =>
auth.isAuthenticated === true ? (
<Component {...props} />
) : (
<Redirect to="/login" />
)
}
/>
);
Also, this onSubmit function of my login form looks like this:
onSubmit(e) {
e.preventDefault();
const user = {
email: this.state.email,
password: this.state.password
};
auth.login(user).then(res => {
if (res) {
this.props.history.push("/dashboard");
}
});
}
Now whenever I input the correct email and password in login form, I successfully redirect to /dashboard route. But if I reload /dashboard route even after login, It again sends me back to login page.
From my understanding, it should stay on the dashboard page as isAuthenticated is set to true after login.
Thus my private route should have worked. Then what am I missing? Also for the record, I am following this tutorial for creating PrivateRoute.
When you reload the page, react state is lost.
You already saved the token when login is succcessfull. So we can take advantage of that token to recover authentication state.
As long as you have a token and the token is not expired yet, the user can stay authenticated.
To check token expiration, we can use jwt-token package.
You can modify your auth.js like this to accomplish this:
import axios from "axios";
import jwt_decode from "jwt-decode";
export const auth = {
isAuthenticated: isValidToken(),
login(user) {
const config = {
headers: {
"Content-Type": "application/json"
}
};
const body = JSON.stringify({ email: user.email, password: user.password });
return axios
.post("http://localhost:5000/userauth/login", body, config)
.then(res => {
localStorage.setItem("token", res.data.token);
this.isAuthenticated = true; //we may remove this line if it works without it
return res.data;
})
.catch(err => {
this.isAuthenticated = false;
console.log(err);
});
}
};
const isValidToken = () => {
const token = localStorage.getItem("token");
if (token && isValid(token)) {
return true;
}
return false;
};
const isValid = token => {
const decoded = jwt_decode(token);
const currentTime = Date.now() / 1000;
if (currentTime > decoded.exp) {
return false;
}
return true;
};
React state resets after refresh.
Your problem is your auth state is resetting after refresh.
You should make sure you re-authenticate yourself everytime when page refreshes, ideally you should do it in routes component in useEffect() or componentDidMount() depending on whether you are using hooks or class.
As #Steve pointed out, you can just grab the token from localStorage.