I am implementing Authorization Code Flow with the Spotify Web API for a simple React app using Node Express for the server and cannot figure out how to pass authentication credentials from the server to the client.
I am using React's useContext hook in order to store authorization credentials.
import React, { createContext, useState, useEffect } from "react";
// the shape of the default value must match
// the shape that consumers expect
// auth is an object, setAuthData is a function
export const AuthContext = createContext({
auth: {},
setAuthData: () => {},
});
const AuthProvider = ({ children }) => {
const [auth, setAuth] = useState({ loading: true, data: null });
const setAuthData = (data) => {
setAuth({ data: data });
};
// on component mount, set the authorization data to
// what is found in local storage
useEffect(() => {
setAuth({
loading: false,
data: JSON.parse(window.localStorage.getItem("authData")),
});
return () => console.log("AuthProvider...");
}, []);
// when authorization data changes, update the local storage
useEffect(() => {
window.localStorage.setItem("authData", JSON.stringify(auth.data));
}, [auth.data]);
return (
<AuthContext.Provider value={{ auth, setAuthData: setAuthData }}>
{children}
</AuthContext.Provider>
);
};
export default AuthProvider;
Inside index.js, I have wrapped my App in AuthProvider
import ReactDOM from "react-dom";
import AuthProvider from "./contexts/AuthContext";
import App from "./Components/App/AppRouter";
import * as serviceWorker from "./serviceWorker";
ReactDOM.render(
<React.StrictMode>
<AuthProvider>
<App />
</AuthProvider>
</React.StrictMode>,
document.getElementById("root")
);
Within the App, I am using react-router-dom to manage routing and the protected route.
// External libraries
import React from "react";
import {
BrowserRouter as Router,
Switch,
Route,
} from "react-router-dom";
import { AuthContext } from "../../contexts/AuthContext";
// Components
import { PrivateRoute } from "../PrivateRoute/PrivateRoute";
function AppRouter(props) {
return (
<Router>
<Switch>
<Route path="/login" component={Login} />
<PrivateRoute path="/" component={AppLite} />
</Switch>
</Router>
);
}
Inside the PrivateRoute, I allow access to the route based on what is in the authentication context.
import React, { useContext } from "react";
import { Route, Redirect } from "react-router-dom";
import { AuthContext } from "../../contexts/AuthContext";
import Layout from "../App/Layout";
export const PrivateRoute = ({ component: Component, ...rest }) => {
const { auth, setAuthData } = useContext(AuthContext);
// if loading is set to true, render loading text
if (auth.loading) {
return (
<Route
render={() => {
return (
<Layout>
<h2>Loading...</h2>
</Layout>
);
}}
/>
);
}
// if the user has authorization render the component,
// otherwise redirect to the login screen
return auth.data ? <Component /> : <Redirect to="/login" />;
};
When the user goes to the home page, they are redirected to the login screen. There, a link redirects the user to /api/login. /api routes are proxied to the node server and /api/login initiates the spotify authorization call. The user is directed to the Spotify login, enters their information and I end up with an access token and refresh token. That all works.
With the access token and refresh token I can redirect the user to a URL with those parameters (e.g. /#/user/${access_token}/${refresh_token}
) but I'm at a loss how I can get those parameters into my authorization context. Note that getting the tokens from the URL is not the problem.
What I've tried to do is add a useEffect to my PrivateRoute which gets the parameters from the URL and then updates the authorization context if they are found.
const { auth, setAuthData } = useContext(AuthContext);
const location = useLocation();
// on mounting the component, check the URL for
// authentication data, if it is present, set
// it on the authorization context
useEffect(() => {
let authData = getHashParams(location);
authData && setAuthData(authData);
return () => {
authData = null;
};
}, [location, setAuthData]);
However this throws things into an infinite loop. I only seem to be able to successfully use setAuthData when triggered by an onClick event. How should I intercept the redirect from my api router so that I can update the data in the authorization context and then go to the PrivateRoute?
Alternatively, is there a way that I can encapsulate all of my api router logic within an onClick event and get the final response back from it (e.g. fetch("/api/login")....user gets redirected, fills in info, exchange code for tokens, send tokens back as response...then((response) => setAuthData(response)...
)?