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) => {
Related
tl;dr:
How does one go about setting up a hook to handle token headers across axios api calls in a meaningfully maintainable way, with the assumption that the token itself is exposed as a hook.
I am currently handling authentication by exposing an access token/permissions in a context, and providing a protected route implementation that conditionally exposes the outlet OR a navigation call based on whether the token exists (which is retrieved from the hook).
Initially this works alright, and every component/hook in my application will have access to the hook to get the token. However, what I really want to do now is gain access to that hook where I make my api calls to set up an axios interceptor to manage the auth header for my api calls.
The issue I'm running into is I think any api call will have to be nested within a hook in order for me to use the token on it, and I'm not really sure what that looks like.
I'm using react-query, and was hoping I'd be able to use a mutation to set something to be accessed throughout the app, but that suffers the same pitfall of needing a component to be able to access the hook.
Is it possible to implement a hook for your token - appending middleware with axios?
the protected route implementation:
import React from 'react';
import { Outlet, useLocation, Navigate } from 'react-router-dom';
import { useAuth } from './AuthProvider';
const ProtectedRouterOutlet = () => {
const { token } = useAuth();
const location = useLocation();
if (!token) {
return <Navigate to="/login" replace state={{ from: location }} />;
}
return <Outlet/>;
};
export default ProtectedRouterOutlet;
Auth provider context wrapper
const AuthContext = React.createContext<any>(null);
export const useAuth = () => {
return React.useContext(AuthContext);
};
const loginApiCall = (userName: string, password: string) =>{
if(!userName || !password) { return Promise.reject('Missing Credentials') }
return axios.post(`${auth_service}/oauth/token`, {username: userName, password: password})
}
const AuthProvider = ({ children }: any) => {
const navigate = useNavigate();
const [token, setToken] = React.useState<string | null>(null);
const location = useLocation();
useEffect(() => {
if(location.pathname === '/login' && token) {
navigate('/');
} else if (!token) {
navigate('/login');
}
}, [token])
const loginCall = useMutation( (data: any) => loginApiCall(data.username, data.password), {onSuccess: token => {
console.log('success', token);
setToken(token.data);
// I could do a settimeout here Or use the useeffect hook
// setTimeout(() => navigate('/'))
}})
const handleLogin = async (username: string, password: string) => {
loginCall.mutate({username, password});
};
const handleLogout = () => {
setToken(null);
// todo: call logout api to invalidate token
};
const value = useMemo(() => ({
token,
onLogin: handleLogin,
onLogout: handleLogout,
}), [token]);
return (
<AuthContext.Provider value={value}>
{children}
</AuthContext.Provider>
);
};
export default AuthProvider;
and the main app file:
const rootComponent = () => {
return (
<QueryClientProvider client={queryClient}>
<AuthProvider>
<Shell>
<Outlet/>
</Shell>
</AuthProvider>
</QueryClientProvider>
);
};
EDIT:
I found this (setting defaults in axios), but I'm not sold on it yet:
useEffect(() => {
if(token) { // setting default common header if token exists
axios.defaults.headers.common = {...axios.defaults.headers.common, Authorization: `Bearer ${token.access_token}`};
}
if(location.pathname === '/login' && token) {
navigate('/');
} else if (!token) {
navigate('/login');
}
}, [token])
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 using the official with-apollo example to create a nextjs frontend. I am trying to use the user's slug, which can be found in the url string to render the user profile. However, I am not able to use the url parameter (the slug) as a variable in the graphql query.
The Link to the user profile
<Link href={{ pathname: "/users/[slug]", query: { slug: user.slug } }}>
The user profile component
import { gql, useQuery } from "#apollo/client"
import ErrorMessage from "./ErrorMessage"
import { useRouter } from "next/router";
export const USER_QUERY = gql`
query getUser($slug: String!) {
user(slug: $slug) {
id
email
}
}
`
// I can not get this to work using url parameters
export const userQueryVars = {
slug: "userSlug", // This should be a url parameter!!
}
export default function UserProfile() {
const router = useRouter()
const userSlug = router.query.slug
const { loading, error, data } = useQuery(USER_QUERY, {
variables: {slug: userSlug},
})
if (error) return <ErrorMessage message="Error loading users." />
if (loading) return <div>Loading</div>
if (!data) return <div>No data</div>
const { user } = data
return (
<section>
<div>
<h3>
{user.firstName} {user.lastName}
</h3>
<p>{user.email}</p>
</div>
</section>
)
}
The user profile page
import App from "../../../components/App"
import Header from "../../../components/Header"
import UserProfile, {
USER_QUERY,
userQueryVars,
} from "../../../components/UserProfile"
import { initializeApollo, addApolloState } from "../../../lib/apolloClient"
const UserProfilePage = () => (
<App>
<Header />
<UserProfile />
</App>
)
export async function getServerSideProps() {
const apolloClient = initializeApollo()
await apolloClient.query({
query: USER_QUERY,
variables: userQueryVars, // This is passed from the component!
})
return addApolloState(apolloClient, {
props: {}
})
}
export default UserProfilePage
What I have tried so far (among a lot of other things):
Using router:
export const userQueryVars = {
slug: router.query.slug,
}
Error: You should only use "next/router" inside the client side of your app.
Using router and checking that is it called on client side:
if (process.browser) {
export const userQueryVars = {
slug: router.query.slug,
}
}
Error: 'import' and 'export' may only appear at the top level.
I would be very thankful for any kind of help!!
When using getServerSideProps you can find your slug (and all other dynamic params if you have them) inside context.params:
export async function getServerSideProps(context) {
const { slug } = context.params;
// Do whatever you need with `slug`
// ...
}
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>
)
}
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
}
})