Expected:
the async function checks if the user is authenticated, then return true, so that the protected component gets rendered, or false, that redirects the user to the login page.
What actually happens:
the getAuth() function returns "Promise ", breaking the code.
export default function RequireAuth({ children, redirectTo }) {
const BASE_API_URL = "https://api-backend.test";
const getAuth = async () => {
const isAuth = await axios
.get(BASE_API_URL + "/user_auth.php", {
withCredentials: true,
})
.then((res) => {
if (res.status === 201) {
return true;
}
})
.catch((err) => {
if (err.response.status === 401) {
return false;
}
});
return isAuth;
};
let isAuthenticated = getAuth();
return isAuthenticated ? children : <Navigate to={redirectTo} />;
}
This is how the "protected" component should be displayed according to the official documentation here.
<Route
path="/dashboard"
element={
<RequireAuth redirectTo="/login">
<Dashboard />
</RequireAuth>
}
/>
You have to do it properly using useEffect, try this
export default function RequireAuth({ children, redirectTo }) {
const BASE_API_URL = "https://api-backend.test";
const [isAuthenticated, setAuthenticated] = useState(false);
useEffect(() => {
const getAuth = () => {
const isAuth = axios
.get(BASE_API_URL + "/user_auth.php", {
withCredentials: true
})
.then((res) => {
if (res.status === 201) {
setAuthenticated(true);
}
})
.catch((err) => {
if (err.response.status === 401) {
setAuthenticated(false);
}
});
};
getAuth();
}, [redirectTo]);
if (isAuthenticated) {
return children;
}
return <Navigate to={redirectTo} />;
}
getAuth is an async function, so it will return a Promise. You have to either await its output or use .then to work with the resolved promise.
Side note, it's usually preferable to either use async/await or .then, but not both.
Related
I have used react-router-dom for Navigation..but My Problem is without authentication also Dashboard Screen is being visible for mili seconds.
App.js
<Route index path="/" element={<ProtectedRoute><Dashboard /></ProtectedRoute>} />
<Route path="/Login" element={<Login />} />
ProtectedRoute
const ProtectedRoute = ({ children }) => {
const { user } = useMyContext();
if (!user) {
return <Navigate to="/Login" />;
}
return children;
};
export default ProtectedRoute;
Login.js
onClick..
await login(data.get('email'), data.get('password'));
navigate('/', { replace: true })
Context.js
function login(email, password) {
return signInWithEmailAndPassword(auth, email, password)
}
function logOut() {
return signOut(auth);
}
useEffect(() => {
const unsubscribe = onAuthStateChanged(auth, (currentuser) => {
setUser(currentuser);
});
return () => {
unsubscribe();
}
}, [])
How can I protect my protected screen from unauthorized access?
The issue is that your ProtectedRoute component doesn't wait for the authentication status to be confirmed. In other words, the default user state masks one of either the authenticated or unauthenticated status.
It should conditionally render a loading indicator while onAuthStateChanged is making the first call to determine the user's authentication status. For the initial user state value use a value that is neither a defined user object in the case of an authenticated user or null in the case of an unauthenticated user. undefined is a good initial value.
Example:
Context
const [user, setUser] = React.useState(); // initially undefined
function login(email, password) {
return signInWithEmailAndPassword(auth, email, password);
}
function logOut() {
return signOut(auth);
}
useEffect(() => {
const unsubscribe = onAuthStateChanged(auth, (currentuser) => {
setUser(currentuser);
});
return unsubscribe;
}, []);
ProtectedRoute
const ProtectedRoute = ({ children }) => {
const { user } = useMyContext();
if (user === undefined) {
return null; // or loading indicator/spinner/etc
}
return user ? children : <Navigate to="/Login" replace />;
};
I'm trying to implement a Protected Route, which firstly tries to get an authentification(api call) so it can display the Route.
But somehow the state value doesnt change..
Do you got any idea?
const ProtectedRoute = ({ component: Component, ...rest }) => {
const [isAuthenticated, setIsAuthenticated] = useState(false);
const fetch = async () => {
const result = await axios.get("http://localhost:5000/auth/", {
withCredentials: true,
});
if (result.status >= 200 && result.status < 300) {
setIsAuthenticated(true);
} else {
setIsAuthenticated(false);
}
};
useEffect(() => {
fetch();
}, []);
return (
<Route
{...rest}
render={(props) => {
if (isAuthenticated) {
return <Component {...props} />;
} else {
return <Redirect to={"./loginUser"} />;
}
}}
/>
);
};
export default ProtectedRoute;
What you can do is to return something when the api call is still loading. Something like this :
const ProtectedRoute = ({ component: Component, ...rest }) => {
const [isAuthenticated, setIsAuthenticated] = useState(undefined);
const [isLoading, setIsLoading] = useState(false);
const fetch = async () => {
const result = await axios.get("http://localhost:5000/auth/", {
withCredentials: true,
});
if (result.status >= 200 && result.status < 300) {
setIsAuthenticated(true);
} else {
setIsAuthenticated(false);
}
};
useEffect(() => {
setIsLoading(true);
fetch();
setIsLoading(false);
}, []);
return (
<Route
{...rest}
render={(props) => {
if(isLoading) { // do something }
else if (isAuthenticated !== undefined && !isLoading) {
return <Component {...props} />;
} else {
return <Redirect to={"./loginUser"} />;
}
}}
/>
);
};
export default ProtectedRoute;
I built a custom hook to get a user from firebase:
export function useAuth() {
const [currentUser, setCurrentUser] = useState<any>();
useEffect(() => {
const unsubscribe = onAuthStateChanged(auth, async (user) => {
if (user === null) {
setCurrentUser(null);
} else {
const u = await getDoc(doc(db, "users", user.uid));
const name = u.data().name;
setCurrentUser(user === null ? null : { ...user, name: name });
}
});
return unsubscribe;
}, []);
return currentUser;
}
In my components I use it as:
const currentUser = useAuth();
useEffect(() => {
const localScore = JSON.parse(localStorage.getItem("score"));
setPush(!(localScore[currentUser.name] === tasks.length));
// ^ TypeError: Cannot read properties of undefined (reading 'name')
}, []);
I think I get the error, because when the components gets rendered the first time, there is no currentUser. But I load the component after the user logged in (what sets the user), thus I am confused.
I think I need to await the user somehow, but how?
From the comments:
I moved my onAuthStateChanged to a context, but this is checking the login state only once and is not updating:
const AppContext = createContext(null);
export function AppWrapper({ children }) {
const [currentUser, setCurrentUser] = useState<any>();
useEffect(() => {
const unsubscribe = onAuthStateChanged(auth, async (user) => {
if (user === null) {
setCurrentUser(null);
} else {
const u = await getDoc(doc(db, "users", user.uid));
const name = u.data().name;
setCurrentUser(user === null ? null : { ...user, name: name });
}
});
return unsubscribe;
}, []);
return (
<AppContext.Provider value={currentUser}>{children}</AppContext.Provider>
);
}
export function useAppContext() {
return useContext(AppContext);
}
function MyApp({ Component, pageProps }) {
const currentUser = useAppContext();
return (
<>
<AppWrapper>
{Component.secure && !currentUser ? (
<Login />
) : (
<>
<Component {...pageProps} />
</>
)}
</AppWrapper>
</>
);
}
I am developing a spotify clone with the ability to play a preview of the songs and display user's different top tracks and artists. I have already made standalone pages for the website after authorizing with the help spotify-web-api-node package, but i am kinda facing a problem connecting the routers, after i login with spotify i reach my profile page where i have links to other pages, but when i try to go to another page i get an error on the server that it is an invalid authorization code and on the web console, the package throws an error that no access token was provided. I have tried every possible way to correct this but i am not able to do anything. Please help me out. The relevant code as well the whole GitHub repository is linked below:
The Github repository for this project is https://github.com/amoghkapoor/Spotify-Clone
App.js
const code = new URLSearchParams(window.location.search).get("code")
const App = () => {
return (
<>
{code ?
<Router>
<Link to="/tracks">
<div style={{ marginBottom: "3rem" }}>
<p>Tracks</p>
</div>
</Link>
<Link to="/">
<div style={{ marginBottom: "3rem" }}>
<p>Home</p>
</div>
</Link>
<Switch>
<Route exact path="/">
<Profile code={code} />
</Route>
<Route path="/tracks">
<TopTracks code={code} />
</Route>
</Switch>
</Router> : <Login />}
</>
)
}
TopTracks.js
const spotifyApi = new SpotifyWebApi({
client_id: "some client id"
})
const TopTracks = ({ code }) => {
const accessToken = useAuth(code)
console.log(accessToken) // undefined in console
console.log(code) // the correct code as provided by spotify
useEffect(() => {
if (accessToken) {
spotifyApi.setAccessToken(accessToken)
return
}
}, [accessToken])
'useAuth' custom Hook
export default function useAuth(code) {
const [accessToken, setAccessToken] = useState()
const [refreshToken, setRefreshToken] = useState()
const [expiresIn, setExpiresIn] = useState()
useEffect(() => {
axios
.post("http://localhost:3001/login", {
code
})
.then(res => {
setAccessToken(res.data.accessToken)
setRefreshToken(res.data.refreshToken)
setExpiresIn(res.data.expiresIn)
window.history.pushState({}, null, "/")
})
.catch((err) => {
// window.location = "/"
console.log("login error", err)
})
}, [code])
You don't appear to be persisting your access/refresh tokens anywhere. As soon as the component is unloaded, the data would be discarded. In addition, a sign in code is only usable once. If you use it more than once, any OAuth-compliant service will invalidate all tokens related to that code.
You can persist these tokens using localStorage, IndexedDB or another database mechanism.
For the purposes of an example (i.e. use something more secure & permanent than this), I'll use localStorage.
To help manage state across multiple views and components, you should make use of a React Context. This allows you to lift common logic higher in your component tree so that it can be reused.
Furthermore, instead of using setInterval to refresh the token periodically, you should only perform refresh operations on-demand - that is, refresh it when it expires.
// SpotifyAuthContext.js
import SpotifyWebApi from 'spotify-web-api-node';
const spotifyApi = new SpotifyWebApi({
clientId: 'fcecfc72172e4cd267473117a17cbd4d',
});
export const SpotifyAuthContext = React.createContext({
exchangeCode: () => throw new Error("context not loaded"),
refreshAccessToken: () => throw new Error("context not loaded"),
get hasToken: spotifyApi.getAccessToken() !== undefined,
api: spotifyApi
});
export const useSpotify = () => useContext(SpotifyAuthContext);
function setStoredJSON(id, obj) {
localStorage.setItem(id, JSON.stringify(obj));
}
function getStoredJSON(id, fallbackValue = null) {
const storedValue = localStorage.getItem(id);
return storedValue === null
? fallbackValue
: JSON.parse(storedValue);
}
export function SpotifyAuthContextProvider({children}) {
const [tokenInfo, setTokenInfo] = useState(() => getStoredJSON('myApp:spotify', null))
const hasToken = tokenInfo !== null
useEffect(() => {
if (tokenInfo === null) return; // do nothing, no tokens available
// attach tokens to `SpotifyWebApi` instance
spotifyApi.setCredentials({
accessToken: tokenInfo.accessToken,
refreshToken: tokenInfo.refreshToken,
})
// persist tokens
setStoredJSON('myApp:spotify', tokenInfo)
}, [tokenInfo])
function exchangeCode(code) {
return axios
.post("http://localhost:3001/login", {
code
})
.then(res => {
// TODO: Confirm whether response contains `accessToken` or `access_token`
const { accessToken, refreshToken, expiresIn } = res.data;
// store expiry time instead of expires in
setTokenInfo({
accessToken,
refreshToken,
expiresAt: Date.now() + (expiresIn * 1000)
});
})
}
function refreshAccessToken() {
return axios
.post("http://localhost:3001/refresh", {
refreshToken
})
.then(res => {
const refreshedTokenInfo = {
accessToken: res.data.accessToken,
// some refreshes may include a new refresh token!
refreshToken: res.data.refreshToken || tokenInfo.refreshToken,
// store expiry time instead of expires in
expiresAt: Date.now() + (res.data.expiresIn * 1000)
}
setTokenInfo(refreshedTokenInfo)
// attach tokens to `SpotifyWebApi` instance
spotifyApi.setCredentials({
accessToken: refreshedTokenInfo.accessToken,
refreshToken: refreshedTokenInfo.refreshToken,
})
return refreshedTokenInfo
})
}
async function refreshableCall(callApiFunc) {
if (Date.now() > tokenInfo.expiresAt)
await refreshAccessToken();
try {
return await callApiFunc()
} catch (err) {
if (err.name !== "WebapiAuthenticationError")
throw err; // rethrow irrelevant errors
}
// if here, has an authentication error, try refreshing now
return refreshAccessToken()
.then(callApiFunc)
}
return (
<SpotifyAuthContext.Provider value={{
api: spotifyApi,
exchangeCode,
hasToken,
refreshableCall,
refreshAccessToken
}}>
{children}
</SpotifyAuthContext.Provider>
)
}
Usage:
// TopTracks.js
import useSpotify from '...'
const TopTracks = () => {
const { api, refreshableCall } = useSpotify()
const [ tracks, setTracks ] = useState([])
const [ error, setError ] = useState(null)
useEffect(() => {
let disposed = false
refreshableCall(() => api.getMyTopTracks()) // <- calls getMyTopTracks, but retry if the token has expired
.then((res) => {
if (disposed) return
setTracks(res.body.items)
setError(null)
})
.catch((err) => {
if (disposed) return
setTracks([])
setError(err)
});
return () => disposed = true
});
if (error != null) {
return <span class="error">{error.message}</span>
}
if (tracks.length === 0) {
return <span class="warning">No tracks found.</span>
}
return (<ul>
{tracks.map((track) => {
const artists = track.artists
.map(artist => artist.name)
.join(', ')
return (
<li key={track.id}>
<a href={track.preview_url}>
{track.name} - {artists}
</a>
</li>
)
}
</ul>)
}
// Login.js
import useSpotify from '...'
const Login = () => {
const { exchangeCode } = useSpotify()
const [ error, setError ] = useState(null)
const code = new URLSearchParams(window.location.search).get("code")
useEffect(() => {
if (!code) return // no code. do nothing.
// if here, code available for login
let disposed = false
exchangeCode(code)
.then(() => {
if (disposed) return
setError(null)
window.history.pushState({}, null, "/")
})
.catch(error => {
if (disposed) return
console.error(error)
setError(error)
})
return () => disposed = true
}, [code])
if (error !== null) {
return <span class="error">{error.message}</span>
}
if (code) {
// TODO: Render progress bar/spinner/throbber for "Signing in..."
return /* ... */
}
// if here, no code & no error. Show login button
// TODO: Render login button
return /* ... */
}
// MyRouter.js (rename it however you like)
import useSpotify from '...'
import Login from '...'
const MyRouter = () => {
const { hasToken } = useSpotify()
if (!hasToken) {
// No access token available, show login screen
return <Login />
}
// Access token available, show main content
return (
<Router>
// ...
</Router>
)
}
// App.js
import SpotifyAuthContextProvider from '...'
import MyRouter from '...'
const App = () => {
return (
<SpotifyAuthContextProvider>
<MyRouter />
</SpotifyAuthContextProvider>
);
}
Problem: I am trying to authenticate a user through isAuth() helpers but it is acting weird. I want it to look for access token if any or call for access token from backend if refresh token is available, and though it works perfectly and sets access token cookie, the issue is if called from PrivateRoutes.jsx, it does not sees the tokens at all and sends the user to login page.
Adding required code for refs:
isAuth():
export const isAuth = () => {
if (window !== undefined) {
const accessCookieChecked = getCookie("_mar_accounts_at");
const refreshCookieChecked = getCookie("_mar_accounts_rt");
if (accessCookieChecked) {
return true;
} else if (refreshCookieChecked) {
console.log(refreshCookieChecked);
axios({
method: "POST",
url: `${API_URL}/api/token`,
data: { refresh_token: refreshCookieChecked },
}).then((res) => {
console.log(res);
setCookie("_mar_accounts_at", res.data.accessToken);
return true;
});
} else {
return false;
}
} else {
return false;
}
};
PrivateRoutes.jsx
import React from "react";
import { Route, Redirect } from "react-router-dom";
import { isAuth } from "../helpers/auth";
const PrivateRoute = ({ component: Component, ...rest }) => (
<Route
{...rest}
render={(props) =>
isAuth() ? (
<Component {...props} />
) : (
<Redirect
to={{ pathname: "/login", state: { from: props.location } }}
/>
)
}
></Route>
);
export default PrivateRoute;
Can someone please see this? And help!
You are running into an async issue most likely, when you make the call in axios, the return true; in the callback never actually returns to your funciton call in the PrivateRoute. Instead, you need to use a Promise/setState/useEffect:
export const isAuth = () => {
if (window === undefined) {
return Promise.resolve(false);
} else {
const accessCookieChecked = getCookie("_mar_accounts_at");
const refreshCookieChecked = getCookie("_mar_accounts_rt");
if (accessCookieChecked) {
return Promise.resolve(true);
} else if (refreshCookieChecked) {
console.log(refreshCookieChecked);
return new Promise(resolve => {
axios({
method: "POST",
url: `${API_URL}/api/token`,
data: { refresh_token: refreshCookieChecked },
}).then((res) => {
console.log(res);
setCookie("_mar_accounts_at", res.data.accessToken);
resolve(true);
});
})
} else {
return Promise.resolve(false);
}
}
};
import React, { useState, useEffect } from 'react';
import { Route, Redirect } from 'react-router-dom';
import { isAuth } from '../helpers/auth';
const PrivateRoute = ({ component: Component, ...rest }) => {
const [isAuthTrue, setIsAuthTrue] = useState();
const [loading, setLoading] = useState(true);
useEffect(() => {
isAuth().then(res => {
setIsAuthTrue(res);
setLoading(false);
})
})
return (
<>
{loading ? (
<div>some loading state</div>
) : (
<Route
{...rest}
render={(props) =>
isAuthTrue ? (
<Component {...props} />
) : (
<Redirect
to={{ pathname: '/login', state: { from: props.location } }}
/>
)
}
/>
)}
</>
);
};
export default PrivateRoute;