How to Implement Magic Auth in an Express + React App

Magic Staff · January 13, 2021
How to Implement Magic Auth in an Express + React App

#Resources

  • The full codebase can be found here

#Quick Start Instructions

#Start server

Bash
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)

Bash
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)

Bash
01REACT_APP_MAGIC_PUBLISHABLE_KEY=pk_live_1234567890
02REACT_APP_SERVER_URL=http://localhost:8080

#.env (server)

Bash
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).

Javascript
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.

Javascript
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.

Javascript
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.

Javascript
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.

Javascript
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.

Javascript
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.

Bash
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.

Javascript
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:

Javascript
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.

Javascript
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!

Let's make some magic!