react-material-passport

Authentication boilerplate with React, Material-UI and Local, Facebook, Google and JWT Passport strategies.

Stars
6

React Material Passport

Features

  • React with JWT authentication with Redux Thunk and Material UI
  • Express with Mongoose
  • Passport with Local, JWT, Facebook and Google strategies

Demo

Demo here.

Screenshots

Installation

Install backend dependencies with:

npm install

Install client dependencies with:

cd client
npm install

In the /config folder you need to create dev.js config file with the following:

module.exports = {
  mongoURI: "mongodb://localhost:27017/react-material-passport-path-to-db",
  googleClientID: "your google client id",
  googleClientSecret: "your google secret",
  googleCallbackURL: "/auth/google/callback",
  facebookAppID: "your facebook app id",
  facebookSecret: "your facebook secret",
  facebookCallbackURL: "/auth/facebook/callback",
  secretOrKey: "secret string for jwt",
  successRedirectURL: "https://localhost:3000"
};

Create certificates with:

cd security
openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout cert.key -out cert.pem -config req.cnf -sha256

Then run the nodemon server with:

npm run server

The server will be available on https://localhost:5000

Run the client with:

cd client
npm run start

The client will be available on https://localhost:3000

or run the both with:

npm run dev

Backend

For Facebook OAuth to work it requires https on local server so we make use of built in https server with:

const port = process.env.PORT || 5000;

const httpsOptions = {
  key: fs.readFileSync("./security/cert.key"),
  cert: fs.readFileSync("./security/cert.pem")
};

const server = https.createServer(httpsOptions, app).listen(port, () => {
  console.log("https server running at " + port);
});

As you can see for this to work we must generate cert.key and cert.pem certificates in security folder, we do that by navigating to /security and executing:

openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout cert.key -out cert.pem -config req.cnf -sha256

In the /security/req.cnf put something like this:

[req]
distinguished_name = req_distinguished_name
x509_extensions = v3_req
prompt = no
[req_distinguished_name]
C = GE
ST = State
L = Location
O = Organization Name
OU = Organizational Unit
CN = www.localhost.com
[v3_req]
keyUsage = critical, digitalSignature, keyAgreement
extendedKeyUsage = serverAuth
subjectAltName = @alt_names
[alt_names]
DNS.1 = www.localhost.com
DNS.2 = localhost.com
DNS.3 = localhost

Now we can specify https://localhost:5000/auth/facebook/callback for callback url in Facebook app settings.

We also mount two static folders, one /static for the images and other assets needed by React, the other one /client/build is used in the production is for serving React's assets and for the all other routes we return /client/build/index.html and let the React do the routing.

// Use Routes
app.use("/", authRoutes);
app.use("/", apiRoutes);
app.use("/static", express.static(__dirname + "/static"));

// Serve static assets if in production
if (process.env.NODE_ENV === "production") {
  // Set static folder
  app.use(express.static("client/build"));

  app.get("*", (req, res) => {
    res.sendFile(path.resolve(__dirname, "client", "build", "index.html"));
  });
...

We make use of https server only in development because for example Heroku wont let us run it on a free plan on the port 5000 so in the production we does the usual on port 80. And it is accessible on https url by default so we can have https callback url.

const port = process.env.PORT || 80;
app.listen(port, () => console.log(`Server started on port ${port}`));

Passport configuration

In the /services folder we have all the Passport strategies, only the local strategy does flashing the error messages. It does it without session, we don't use session because we use JWT token in the x-auth-token header to auth API calls. For the flashing messages to work we make custom callback in middleware/requireLocalAuth.js:

const requireLocalAuth = function(req, res, next) {
  passport.authenticate("local", function(err, user, info) {
    if (err) {
      return next(err);
    }
    if (!user) {
      return res.status(422).send(info); // flash the error message
    }
    req.user = user;
    next();
  })(req, res, next);
};

After user signs in Facebook redirects him to the specified callback route from which we have to both redirect and generate and send JWT token. We do that by sending token via cookie and redirect to home page. That is the only time we use cookie. After that React parses token from the cookie, deletes the cookie and stores it in the local storage and from now on sends it via header.

router.get(
  keys.facebookCallbackURL,
  passport.authenticate("facebook", {
    failureRedirect: "/",
    session: false
  }),
  (req, res) => {
    const token = tokenFromUser(req.user);
    res.cookie("x-auth-cookie", token);
    res.redirect(keys.successRedirectURL);
  }
);

We does the same with other two strategies, Google and Local so we could have unified interface for authentication. Don't forget to pass email scope in the authenticate call so the user is prompted to give us the email also.

router.get(
  "/auth/facebook",
  passport.authenticate("facebook", {
    scope: ["public_profile", "email"]
  })
);

We make use of two auth middlewares, requireLocalAuth which is called when user tries to log in with email and password and requireJwtAuth which is used for all other API calls in the /routes/api.js routes.

Frontend

Logging in

First we assume that user just log on and has cookie so we parse the token from cookie and call const response = await axios.get("/api/user", { headers });. If that succeeds token is valid and we set the token in the local storage and delete the cookie.

The other scenario is that the user is already authenticated and has the token in the local storage so we again we call /api/user route and if it succeeds token is valid and we put the user in the store.

export const logInUser = () => async (dispatch, getState) => {
  try {
    // register
    const cookieJwt = Cookies.get("x-auth-cookie");
    if (cookieJwt) {
      const headers = {
        "Content-Type": "application/json",
        "x-auth-token": cookieJwt
      };

      const response = await axios.get("/api/user", { headers });
      localStorage.setItem("token", cookieJwt);
      Cookies.remove("x-auth-cookie"); //delete just that cookie

      dispatch({
        type: LOGIN_USER,
        payload: response.data.user
      });
      return;
    }
    // logged in
    const token = localStorage.getItem("token");
    if (token) {
      const headers = {
        "Content-Type": "application/json",
        "x-auth-token": token
      };

      const response = await axios.get("/api/user", { headers });
      dispatch({
        type: LOGIN_USER,
        payload: response.data.user
      });
      return;
    }
  } catch (err) {
    localStorage.removeItem("token");
    Cookies.remove("x-auth-cookie");
    dispatch({
      type: SET_ERROR,
      payload: err.response.data
    });
  }
};

That action creator is called in the /client/src/layout/Navbar.js component in the componentDidMount() method.

For the Local strategy is used Register/Login form which design is used from the Devias.io repo.