Guide

Build Magic auth into your React + Express app

Build Magic auth into your React + Express app

13 January 2021

Resources

See the live demo https://magic-react-express.herokuapp.com/login

The full codebase can be found here.

Quick Start Instructions

Start server

$ git clone https://github.com/magiclabs/example-react-express.git
$ cd example-react-express
$ mv .env.example .env // enter your Magic Secret API Key
$ yarn install
$ node server.js // starts server on localhost:8080

Start client (in a new CLI tab)

$ cd client
$ mv .env.example .env // enter your Magic Publishable API Key
$ yarn install
$ 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.

├── README.md
├── client
│   ├── package.json
│   ├── public
│   │   └── (static files, such as images)
│   ├── src
│   │   ├── App.js
│   │   ├── components
│   │   │   ├── callback.js
│   │   │   ├── email-form.js
│   │   │   ├── header.js
│   │   │   ├── home.js
│   │   │   ├── layout.js
│   │   │   ├── loading.js
│   │   │   ├── login.js
│   │   │   ├── profile.js
│   │   │   └── social-logins.js
│   │   ├── index.js
│   │   └── lib
│   │       ├── UserContext.js
│   │       └── magic.js
│   └── yarn.lock
├── package.json
├── server.js
└── 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 Test Publishable Key such as REACT_APP_MAGIC_PUBLISHABLE_KEY=pk_test_1234567890 and in .env enter your Test Secret Key such as MAGIC_SECRET_KEY=sk_test_1234567890.

client/.env (client)

REACT_APP_MAGIC_PUBLISHABLE_KEY=pk_test_1234567890
REACT_APP_SERVER_URL=http://localhost:8080

.env (server)

MAGIC_SECRET_KEY=sk_test_1234567890
CLIENT_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. Test 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.

magic-allowlist-url

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.

// If isLoggedIn is true, set the UserContext with user data
// Otherwise, set it to {user: null}
useEffect(() => {
  setUser({ loading: true });
  magic.user.isLoggedIn().then(isLoggedIn => {
    return isLoggedIn ? magic.user.getMetadata().then(userData => setUser(userData)) : setUser({ user: null });
  });
}, []);

return (
  <Router>
    <Switch>
      <UserContext.Provider value={[user, setUser]}>
        <Layout>
          <Route path="/" exact component={Home} />
          <Route path="/login" component={Login} />
          <Route path="/profile" component={Profile} />
          <Route path="/callback" component={Callback} />
        </Layout>
      </UserContext.Provider>
    </Switch>
  </Router>
);

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.

async function handleLoginWithEmail(email) {
  // Trigger Magic link to be sent to user
  const didToken = await magic.auth.loginWithMagicLink({
    email,
    redirectURI: new URL('/callback', window.location.origin).href, // optional redirect back to your app after magic link is clicked
  });

  // Validate didToken with server
  const res = await fetch(`${process.env.REACT_APP_SERVER_URL}/api/login`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      Authorization: 'Bearer ' + didToken,
    },
  });

  if (res.status === 200) {
    // Set the UserContext to the now logged in user
    const userMetadata = await magic.user.getMetadata();
    await setUser(userMetadata);
    history.push('/profile');
  }
}

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.

async function handleLoginWithSocial(provider) {
  await magic.oauth.loginWithRedirect({
    provider,
    redirectURI: new URL('/callback', window.location.origin).href, // required redirect to finish social login
  });
}

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 completeing the email login.

// The redirect contains a `provider` query param if the user is logging in with a social provider
useEffect(() => {
  const provider = new URLSearchParams(props.location.search).get('provider');
  provider ? finishSocialLogin() : finishEmailRedirectLogin();
}, [props.location.search]);

// `getRedirectResult()` returns an object with user data from Magic and the social provider
const finishSocialLogin = async () => {
  const result = await magic.oauth.getRedirectResult();
  authenticateWithServer(result.magic.idToken);
};

// `loginWithCredential()` returns a didToken for the user logging in
const finishEmailRedirectLogin = () => {
  const magicCredential = new URLSearchParams(props.location.search).get('magic_credential');
  if (magicCredential) magic.auth.loginWithCredential().then(didToken => authenticateWithServer(didToken));
};

// Send token to server to validate
const authenticateWithServer = async didToken => {
  const res = await fetch(`${process.env.REACT_APP_SERVER_URL}/api/login`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      Authorization: 'Bearer ' + didToken,
    },
  });

  if (res.status === 200) {
    // Set the UserContext to the now logged in user
    const userMetadata = await magic.user.getMetadata();
    await setUser(userMetadata);
    history.push('/profile');
  }
};

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.

const logout = () => {
  magic.user.logout().then(() => {
    setUser({ user: null });
    history.push('/login');
  });
};

Server

Validating the Auth Token (didToken)

In the /api/login route, simply verify the didToken, then send a 200 back to the client.

app.post('/api/login', async (req, res) => {
  try {
    const didToken = req.headers.authorization.substr(7);
    await magic.token.validate(didToken);
    res.status(200).json({ authenticated: true });
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

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.

$ heroku create

Creating app... done, ⬢ blooming-plateau-25608
https://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.

heroku-env-vars

Server.js Update

Add the following in server.js so Heroku knows how to build your app.

// For heroku deployment
if (process.env.NODE_ENV === 'production') {
  app.use(express.static('client/build'));
  app.get('*', (req, res) => {
    res.sendFile(path.resolve(__dirname, 'client', 'build', 'index.html'));
  });
}

Package.json Update

In package.json, add this to the scripts object:

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

$ git add .
$ git commit -m 'your message'
$ 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!