How to Implement Magic Auth in an Express + React App
#Resources
#Quick Start Instructions
#Start server
01$ git clone https://github.com/magiclabs/example-react-express.git
02$ cd example-react-express
03$ mv .env.example .env // enter your Magic Secret API Key
04$ yarn install
05$ node server.js // starts server on localhost:8080
#Start client (in a new CLI tab)
01$ cd client
02$ mv .env.example .env // enter your Magic Publishable API Key
03$ yarn install
04$ yarn start // starts frontend on localhost:3000
#Introduction
This tutorial will show you how to integrate Magic authentication (both magic links and social logins) using one of the most popular tech stacks in web development: React, Express and Node.js. And at the end, we'll deploy the app to Heroku.
Note: the tutorial was built using Magic UI components. If you swap them out for your own custom CSS, you can delete @magiclabs/ui
and framer-motion
from your client/package.json
dependencies.
#File Structure
The root directory will contain the server-side files. And inside client
will be the frontend files.
01├── README.md
02├── client
03│ ├── package.json
04│ ├── public
05│ │ └── (static files, such as images)
06│ ├── src
07│ │ ├── App.js
08│ │ ├── components
09│ │ │ ├── callback.js
10│ │ │ ├── email-form.js
11│ │ │ ├── header.js
12│ │ │ ├── home.js
13│ │ │ ├── layout.js
14│ │ │ ├── loading.js
15│ │ │ ├── login.js
16│ │ │ ├── profile.js
17│ │ │ └── social-logins.js
18│ │ ├── index.js
19│ │ └── lib
20│ │ ├── UserContext.js
21│ │ └── magic.js
22│ └── yarn.lock
23├── package.json
24├── server.js
25└── yarn.lock
#Magic Setup
Your Magic setup will depend on what login options you want. For magic links, minimal setup is required. For social logins, follow our documentation for configuration instructions.
Once you have social logins configured (if applicable), grab your API keys from the Magic dashboard. In client/.env
enter your Publishable API Key such as REACT_APP_MAGIC_PUBLISHABLE_KEY=pk_live_1234567890
and in .env
enter your Secret Key such as MAGIC_SECRET_KEY=sk_live_1234567890
.
#client/.env (client)
01REACT_APP_MAGIC_PUBLISHABLE_KEY=pk_live_1234567890
02REACT_APP_SERVER_URL=http://localhost:8080
#.env (server)
01MAGIC_SECRET_KEY=sk_live_1234567890
02CLIENT_URL=http://localhost:3000
For added security, in Magic's dashboard settings, you can specify the URLs that are allowed to use your live API keys. Publishable API keys are always allowed to be used on localhost, however it will block your Live API keys from working anywhere except the URLs specifically added to your allow list.
#Client
#Keeping Track of the User
This app will keep track of the logged in user by using React's useContext
hook. Inside App.js
, wrap the entire app in <UserContext.Provider>
so all child components have access to see if the user is logged in or not. Once a user logs in with Magic, unless they log out, they'll remain authenticated for 7 days by default (the session length is customizable by the developer through the Magic dashboard).
01// If isLoggedIn is true, set the UserContext with user data
02// Otherwise, set it to {user: null}
03useEffect(() => {
04 setUser({ loading: true });
05 magic.user.isLoggedIn().then(isLoggedIn => {
06 return isLoggedIn ? magic.user.getMetadata().then(userData => setUser(userData)) : setUser({ user: null });
07 });
08}, []);
09
10return (
11 <Router>
12 <Switch>
13 <UserContext.Provider value={[user, setUser]}>
14 <Layout>
15 <Route path="/" exact component={Home} />
16 <Route path="/login" component={Login} />
17 <Route path="/profile" component={Profile} />
18 <Route path="/callback" component={Callback} />
19 </Layout>
20 </UserContext.Provider>
21 </Switch>
22 </Router>
23);
#Magic Link Auth
In client/src/components/login.js
, handle magic.auth.loginWithMagicLink()
which is what triggers the magic link to be emailed to the user. It takes an object with two parameters, email
and an optional redirectURI
. Magic allows you to configure the email link to open up a new tab, bringing the user back to your application. With the redirect in place, a user will get logged in on both the original and new tab. Once the user clicks the email link, send the didToken
to the server endpoint at /api/login
to validate it, and if the token is valid, set the UserContext
and redirect to the profile page.
01async function handleLoginWithEmail(email) {
02 // Trigger Magic link to be sent to user
03 const didToken = await magic.auth.loginWithMagicLink({
04 email,
05 redirectURI: new URL('/callback', window.location.origin).href, // optional redirect back to your app after magic link is clicked
06 });
07
08 // Validate didToken with server
09 const res = await fetch(`${process.env.REACT_APP_SERVER_URL}/api/login`, {
10 method: 'POST',
11 headers: {
12 'Content-Type': 'application/json',
13 Authorization: 'Bearer ' + didToken,
14 },
15 });
16
17 if (res.status === 200) {
18 // Set the UserContext to the now logged in user
19 const userMetadata = await magic.user.getMetadata();
20 await setUser(userMetadata);
21 history.push('/profile');
22 }
23}
#Social Logins
The social login implementation is similar. magic.oauth.loginWithRedirect()
takes an object with a provider
, and a required redirectURI
for where to redirect back to once the user authenticates with the social provider and with Magic. In this case, the user will get redirected to http://localhost:3000/callback
.
01async function handleLoginWithSocial(provider) {
02 await magic.oauth.loginWithRedirect({
03 provider,
04 redirectURI: new URL('/callback', window.location.origin).href, // required redirect to finish social login
05 });
06}
#Handling Redirect
After the user authenticates through magic link or social login, Magic will redirect the user to /callback
with several query parameters. You can check if the query parameters include a provider
, and if so, finish the social login, otherwise, it’s a user completing the email login.
01// The redirect contains a `provider` query param if the user is logging in with a social provider
02useEffect(() => {
03 const provider = new URLSearchParams(props.location.search).get('provider');
04 provider ? finishSocialLogin() : finishEmailRedirectLogin();
05}, [props.location.search]);
06
07// `getRedirectResult()` returns an object with user data from Magic and the social provider
08const finishSocialLogin = async () => {
09 const result = await magic.oauth.getRedirectResult();
10 authenticateWithServer(result.magic.idToken);
11};
12
13// `loginWithCredential()` returns a didToken for the user logging in
14const finishEmailRedirectLogin = () => {
15 const magicCredential = new URLSearchParams(props.location.search).get('magic_credential');
16 if (magicCredential) magic.auth.loginWithCredential().then(didToken => authenticateWithServer(didToken));
17};
18
19// Send token to server to validate
20const authenticateWithServer = async didToken => {
21 const res = await fetch(`${process.env.REACT_APP_SERVER_URL}/api/login`, {
22 method: 'POST',
23 headers: {
24 'Content-Type': 'application/json',
25 Authorization: 'Bearer ' + didToken,
26 },
27 });
28
29 if (res.status === 200) {
30 // Set the UserContext to the now logged in user
31 const userMetadata = await magic.user.getMetadata();
32 await setUser(userMetadata);
33 history.push('/profile');
34 }
35};
#Logout
Users also need to be able to log out. In header.js
, add a logout
function to end the user's session with Magic, clear the user from the UserContext, and redirect back to the login page.
01const logout = () => {
02 magic.user.logout().then(() => {
03 setUser({ user: null });
04 history.push('/login');
05 });
06};
#Server
#Validating the Auth Token (didToken)
In the /api/login
route, simply verify the didToken
, then send a 200
back to the client.
01app.post('/api/login', async (req, res) => {
02 try {
03 const didToken = req.headers.authorization.substr(7);
04 await magic.token.validate(didToken);
05 res.status(200).json({ authenticated: true });
06 } catch (error) {
07 res.status(500).json({ error: error.message });
08 }
09});
#Deploying to Heroku
#Create your Heroku Project
When you're ready to deploy your app on Heroku, run heroku create
to generate a new Heroku project. It will return your Heroku app URL, similar to what is shown below.
01$ heroku create
02
03Creating app... done, ⬢ blooming-plateau-25608
04https://blooming-plateau-25608.herokuapp.com/ | https://git.heroku.com/blooming-plateau-25608.git
#Setting .env (config) Vars in Heroku
Then find your new project on heroku.com, and enter your environment variables into your new website's Settings page.
#Server.js Update
Add the following in server.js
so Heroku knows how to build your app.
01// For heroku deployment
02if (process.env.NODE_ENV === 'production') {
03 app.use(express.static('client/build'));
04 app.get('*', (req, res) => {
05 res.sendFile(path.resolve(__dirname, 'client', 'build', 'index.html'));
06 });
07}
#Package.json Update
In package.json
, add this to the scripts
object:
01"heroku-postbuild": "NPM_CONFIG_PRODUCTION=false npm install --prefix client && npm run build --prefix client"
Then you're ready! Run the following commands to deploy your application.
01$ git add .
02$ git commit -m 'your message'
03$ git push heroku master
Now you have a full stack React, Express, Node.js app complete with auth and that's deployed to Heroku!