I am authenticating my NextJS frontend from a backend that gives me an accessToken on a successful email / password login (Laravel Sanctum). From there I am saving that accessToken in local storage.
If i have a page that needs protecting, for instance /profile, i need to verify that the token is valid before showing the page. If it is not valid, they need to be redirected to the /signin page. So i have the following code which does that.
import { useRouter } from 'next/router';
import { useEffect } from 'react';
export default function Profile() {
const router = useRouter();
useEffect(async () => {
const token = localStorage.getItem('accessToken');
const resp = await fetch('https://theapiuri/api/user', {
method: 'GET',
headers: {
'Authorization': 'Bearer ' + token
}
});
const json = await resp.json();
if (!token && json.status !== 200) {
router.push('/signin');
}
})
return (
<div>
<h1>Protected Profile Page</h1>
</div>
)
}
It works, sort of. If I am logged out, and i try to visit /profile it will flash up the profile page for a second or so and then redirect to signin.
This doesn't look good at all. I was wondering if anyone in the same situation could share their solution, or if anyone has some advice that would be greatly appreciated.
Your basic problem is that you are returning the profile page immediately, but the token authentication is async. You should wait for the authentication to happen before showing the page. There's different ways to do that, but a basic way is to just set a variable in your state and then change what is returned by the render function based on that variable.
As an example, here I suppose that you have some component that just shows a loader or spinner or something like that:
import { useRouter } from 'next/router';
import { useEffect, useState } from 'react';
import LoaderComponent from 'components/Loader';
export default function Profile() {
const router = useRouter();
const [hasAccess, setHasAccess] = useState(false);
useEffect(async () => {
const token = localStorage.getItem('accessToken');
const resp = await fetch('https://theapiuri/api/user', {
method: 'GET',
headers: {
'Authorization': 'Bearer ' + token
}
});
const json = await resp.json();
if (!token && json.status !== 200) {
router.push('/signin');
} else {
setHasAccess(true);
}
})
if (!hasAccess) {
return (
<LoaderComponent />
);
}
return (
<div>
<h1>Protected Profile Page</h1>
</div>
)
}
Related
I have a simple project that I built that protects the routes/pages of the website by using the if and else statement and putting each page with a function withAuth(), but I'm not sure if that is the best way to protect routes with nextjs, and I noticed that there is a delay in protecting the route or pages, like 2-3 seconds long, in which they can see the content of the page before it redirects the visitor or unregistered user to the login page.
Is there a way to get rid of it or make the request faster so that unregistered users don't view the page's content? Is there a better approach to safeguard a certain route in the nextjs framework?
Code
import { useContext, useEffect } from "react";
import { AuthContext } from "#context/auth";
import Router from "next/router";
const withAuth = (Component) => {
const Auth = (props) => {
const { user } = useContext(AuthContext);
useEffect(() => {
if (!user) Router.push("/login");
});
return <Component {...props} />;
};
return Auth;
};
export default withAuth;
Sample of the use of withAuth
import React from "react";
import withAuth from "./withAuth";
function sample() {
return <div>This is a protected page</div>;
}
export default withAuth(sample);
you can make the authentication of user on server-side, if a user is logged in then show them the content of the protected route else redirect them to some other route. refer to this page for mote info.
in getServerSideProps check whether the user has logged in
if (!data.username) {
return {
redirect: {
destination: '/accounts/login',
permanent: false,
},
}
}
here's complete example of protected route page
export default function SomeComponent() {
// some content
}
export async function getServerSideProps({ req }) {
const { token } = cookie.parse(req.headers.cookie)
const userRes = await fetch(`${URL}/api/user`, {
method: 'GET',
headers: {
'Authorization': `Bearer ${token}`
}
})
const data = await userRes.json()
// does not allow access to page if not logged in
if (!data.username) {
return {
redirect: {
destination: '/accounts/login',
permanent: false,
},
}
}
return {
props: { data }
}
}
With Customized 401 Page
We are going to first define our customized 401 page
import React from "react"
const Page401 = () => {
return (
<React.Fragment>
//code of your customized 401 page
</React.Fragment>
)
}
export default Page401
Now, we are going to change a small part of the code kiranr shared
export async function getServerSideProps({ req }) {
const { token } = cookie.parse(req.headers.cookie)
const userRes = await fetch(`${URL}/api/user`, {
method: 'GET',
headers: {
'Authorization': `Bearer ${token}`
}
})
const data = await userRes.json()
// does not allow access to page if not logged in
if (!data.username) {
//THIS PART CHANGES
return {
props: {
unauthorized: true
}
}
//THIS PART CHANGES
}
return {
props: { data }
}
}
Then we will check this 'unauthorized' property in our _app.js file and call our customized 401 page component if its value is true
import Page401 from "../components/Error/Server/401/index";
const App = ({ Component, pageProps }) => {
//code..
if (pageProps.unauthorized) {
//if code block reaches here then it means the user is not authorized
return <Page401 />;
}
//code..
//if code block reaches here then it means the user is authorized
return (
<Provider store={store}>
<Component {...pageProps} />
</Provider>
)
}
I am implementing a basic login in react, however when the token is saved in sessionStorage the page does not refresh normally like when I do it with hooks.
I use this component to save and return the token when the login is correct.
//UseToken.js
import { useState } from 'react';
export default function useToken() {
const getToken = () => {
const tokenString = sessionStorage.getItem('token');
const userToken = JSON.parse(tokenString);
return userToken
};
const [token, setToken] = useState(getToken());
const saveToken = userToken => {
sessionStorage.setItem('token', JSON.stringify(userToken));
setToken(userToken.token);
};
return {
setToken: saveToken,
token
}
}
Later, in the app component, I ask for the status of the token in order to render either the login view or the application view.
//App.js
const {token, setToken} = useToken();
if(!token) {
return <Login setToken={setToken} />
}
Then the token is saved in sessionStorage, however the login is still rendered.
Help
The answer is that, when you set the token, you set it as a non-existent object instead of just passing the value to it.
const saveToken = userToken => {
sessionStorage.setItem('token', JSON.stringify(userToken));
setToken(userToken); // instead of userToken.token
};
I am using Okta-React for authentication in my React project and when I run the React test server my login authenticates successfully and redirects to the account page. When I run the React build command and render the build files with Django, my login authenticates properly, but when it redirects back to my site I get a blank /implicit/callback page, no login token or user info, and the code & state gets stuck in the URL. Does anyone know why this is only happening when using Django, and what I can do to resolve this issue?
Here is my authConfig:
const config = {
issuer: 'https://dev-#######.okta.com/oauth2/default',
redirectUri: window.location.origin + '/implicit/callback',
clientId: '#################',
pkce: true
};
export default config;
Here is my accountAuth
import React, { useState, useEffect } from 'react';
import { useOktaAuth } from '#okta/okta-react';
import '../scss/sass.scss';
import "../../node_modules/bootstrap/scss/bootstrap.scss";
import 'react-bootstrap';
const AccountAuth = () => {
const { authState, authService } = useOktaAuth();
const [userInfo, setUserInfo] = useState(null);
useEffect(() => {
if (!authState.isAuthenticated) {
// When user isn't authenticated, forget any user info
setUserInfo(null);
} else {
authService.getUser().then((info) => {
setUserInfo(info);
});
}
}, [authState, authService]); // Update if authState changes
localStorage.setItem("username", userInfo && userInfo.given_name)
const login = async () => {
// Redirect to '/account_page' after login
localStorage.setItem("accountLink", "/account_page")
localStorage.setItem("loginPostingVisibilityStyle", { display: "none" })
localStorage.setItem("postingVisibleStyle", { display: 'block' })
authService.login('/auth_index');
}
const logout = async () => {
// Redirect to '/' after logout
localStorage.setItem("username", null)
localStorage.setItem("accountLink", "/auth_index")
localStorage.setItem("loginPostingVisibilityStyle", { display: "block" })
localStorage.setItem("postingVisibleStyle", { display: 'none' })
authService.logout('/');
}
return authState.isAuthenticated ?
<button className="settings-index" onClick={logout}>Logout</button> :
<button className="settings-index" onClick={login}>Login</button>;
};
export default AccountAuth;
Here is an example of the URL when it's stuck
http://localhost:8000/implicit/callback?code=-mRoU2jTR5HAFJeNVo_PVZsIj8qXuB1-aioFUiZBlWo&state=c9RXCvEgQ4okNgp7C7wPkI62ifzTakC0Ezwd8ffTEb29g5fNALj7aQ63fjFNGGhT
It doesn't look like you're handling the callback to exchange the authorization_code for tokens. You might want to check out Okta's React sample app to see how it works.
I have a React app that is making calls to an API. I have a Client component to handle the calls, and the Components can access it like this (this example is in the componentDidMount function of the Home page, where I want to get a list of all this user's items):
componentDidMount() {
let userId= this.context.userId;
var url = "items/getAllMyItems/" + userId;
Client.fetchData(url, data => {
this.setState({items: data});
});
}
The current setup has no security (just for testing purposes) and the Client is defined like this (this is index.js):
function fetchData(fetchPath, cb) {
return fetch(`https://api.url/${fetchPath}`, {accept: "application/json"})
.then(cb);
}
(there are a couple of other functions which check the results etc, but I've left them out for brevity).
Now, my app connects to Firebase for handling authentication. I have A Firebase component which has 3 files:
firebase.js:
import app from 'firebase/app';
import 'firebase/auth';
const config = {
apiKey: /* etc */,
};
class Firebase {
constructor() {
app.initializeApp(config);
this.auth = app.auth();
}
// *** Auth API ***
doSignInWithEmailAndPassword = (email, password) =>
this.auth.signInWithEmailAndPassword(email, password);
doSignOut = () => this.auth.signOut();
}
export default Firebase;
context.js:
import React from 'react';
const FirebaseContext = React.createContext(null);
export const withFirebase = Component => props => (
<FirebaseContext.Consumer>
{firebase => <Component {...props} firebase={firebase} />}
</FirebaseContext.Consumer>
);
export default FirebaseContext;
index.js:
import FirebaseContext, { withFirebase } from './context';
import Firebase from './firebase';
export default Firebase;
export { FirebaseContext, withFirebase };
We're now implementing backend security, and I need to pass the Firebase token to the API when making calls. I can't figure out how to do it properly.
I know I need to call
firebase.auth().currentUser.getIdToken(true).then(function(idToken) {
// API call with Authorization: Bearer `idToken`
}).catch(function(error) {
// Handle error
});
so I figured that Client/index.js would need to change to something like:
import react from 'react';
import { FirebaseContext } from '../Firebase';
function fetchData(fetchPath, cb) {
<FirebaseContext.Consumer>
{firebase => {
firebase.auth().currentUser.getIdToken(true)
.then(function(idToken) {
// API call with Authorization: Bearer `idToken`
return fetch(`https://api.url/${fetchPath}`, {accept: "application/json"})
.then(cb);
}).catch(function(error) {
// Handle error
});
}}
</FirebaseContext.Consumer>
}
but if I do this I get the error "Expected an assignment or function call but instead saw the expression". I realize this is because it's expecting me to return a component, but I don't want to do that as there's nothing to return. I also tried using useContext, and changing fetchData to:
const Client = () => {
const firebase = useContext(FirebaseContext);
firebase.auth().currentUser.getIdToken(true)
.then(function(idToken) {
// API call with Authorization: Bearer `idToken`
fetch(`https://api.url/${fetchPath}`, {accept: "application/json"})
.then(cb);
}).catch(function(error) {
// Handle error
});
}
but I got an error about an Invalid Hook Call.
Can anyone point me in the right direction?
The code you have to get the ID token looks fine to me.
How to pass it to the API depends on what that API expects, but since you mention ```Authorization: Bearer idToken `` that would typically look like this:
fetch(`https://api.url/${fetchPath}`, {
headers: {
'Accept': 'application/json',
'Authorization': 'Bearer ' + idToken
}
})
I am using the react-spotify-login package and when trying to authorize the application I can't retrieve the access token. My routing works and sending the request works. I just can't retrieve the token. I've just started learning react so I'm hoping it isn't something I'm easily overlooking.
import React, { Component } from 'react';
import SpotifyLogin from 'react-spotify-login';
import { clientId, redirectUri } from '../../Settings';
import { Redirect } from 'react-router-dom';
export class Login extends Component {
render() {
const onSuccess = ({ response }) => {
//const { access_token: token } = response;
console.log("[onSuccess]" + response);
return <Redirect to='/home' />
};
const onFailure = response => console.error("[onFailure]" + response);
return (
<div>
<SpotifyLogin
clientId={clientId}
redirectUri={redirectUri}
onSuccess={onSuccess}
onFailure={onFailure}
/>
</div>
);
}
}
export default Login;
In your approach you are trying to destructure the response data/object and pull field 'response' which does not exist i.e undefined
Change
const onSuccess = ({ response }) => {
to
const onSuccess = (response) => {