神刀安全网

Build a React + Flux App with User Authentication

The React ecosystem is huge, and there are many options for modules which aim to solve the tricky parts of making React work well. Most real-world React apps share some common requirements, and these requirements revolve around state management and routing. Arguably the most common options for these needs are Flux and React Router.

Ken has anawesome series on React and Flux here at Scotch, and there’s certainly no shortage of instruction on these topics around the web. However, there are some other things we need to think about when it comes to building a real-world React app that aren’t covered as often: how to call a remote API and how to authenticate users.

In this tutorial we’ll build a simple contacts application that receives data from an API. We’ll use an Express (NodeJS) server for this, but it should be noted early on that it isn’t necessary to use Node. Rather, we can use any kind of server we want, as long as it can output JSON data.

The best way to do user authentication for any single page app is with JSON Web Tokens (JWT) . There are a lot of details involved with setting up JWT authentication from scratch, so instead we’ll useAuth0.

With Auth0, we can just drop in a script tag and we get immediate access to alogin box and features likesocial login, multifactor authentication , and more.

When wesign up for Auth0, we get a free account that gives us 7,000 free users and two social identity providers . The best part is that this account is production-ready, so we have everything we need to get going with a real-world app.

Build a React + Flux App with User Authentication

Let’s get started!

Set Up a New React Project

In this tutorial we’re going to use React with ES2015, and this means that we’ll need a transpiler to make sure we get all the features we expect and that it will work across all browsers. We’ll use webpack for this, and the easiest way to get started with new React + Webpack project is to use a generator such as Yeoman.

npm install -g yo npm install -g generator-react-webpack mkdir react-auth && cd react-auth yo react-webpack

We get some prompts to follow, and at the end of it we should have a new React project wired up with webpack.

We’ll need some extra dependencies that don’t come with a Yeoman installation, so let’s grab those now.

npm install flux react-router bootstrap react-bootstrap keymirror superagent

We need to tweak our url-loader in our webpack config slightly so that we can make use of React Bootstrap.

// cfg/defaults.js  ...  {   test: //.(png|woff|woff2|eot|ttf|svg)$/,   loader: 'url-loader?limit=8192' },  ...

Another thing we should do right off the bat is make a change to the route that webpack uses to serve the project, or else we’ll get some issues with React Router. Open up server.js and down at the bottom, change:

open('http://localhost:' + config.port + '/webpack-dev-server/');

to

open('http://localhost:' + config.port);

Create an Express Server

Let’s create our Express server right as we get started so it will be ready to be consumed by our React app. This is actually a rather simple server and we just need a few modules to support it.

mkdir react-auth-server && cd react-auth-server npm init npm install express express-jwt cors lodash touch server.js

The express-jwt package that we’re bringing in will be used to create our authentication middleware to protect any API endpoints we want.

 // server.js  const express = require('express'); const app = express(); const jwt = require('express-jwt'); const cors = require('cors'); const _ = require('lodash');  app.use(cors());  // Authentication middleware provided by express-jwt. // This middleware will check incoming requests for a valid // JWT on any routes that it is applied to. const authCheck = jwt({   secret: new Buffer('YOUR_AUTH0_SECRET', 'base64'),   audience: 'YOUR_AUTH0_CLIENT_ID' });  var contacts = [   {     id: 1,     name: 'Chris Sevilleja',     email: 'chris@scotch.io',     image: '//gravatar.com/avatar/8a8bf3a2c952984defbd6bb48304b38e?s=200'   },   {     id: 2,     name: 'Nick Cerminara',     email: 'nick@scotch.io',     image: '//gravatar.com/avatar/5d0008252214234c609144ff3adf62cf?s=200'   },   {     id: 3,     name: 'Ado Kukic',     email: 'ado@scotch.io',     image: '//gravatar.com/avatar/99c4080f412ccf46b9b564db7f482907?s=200'   },   {     id: 4,     name: 'Holly Lloyd',     email: 'holly@scotch.io',     image: '//gravatar.com/avatar/5e074956ee8ba1fea26e30d28c190495?s=200'   },   {     id: 5,     name: 'Ryan Chenkie',     email: 'ryan@scotch.io',     image: '//gravatar.com/avatar/7f4ec37467f2f7db6fffc7b4d2cc8dc2?s=200'   } ];  var contactMin = []; contacts.forEach(contact => {   contactMin.push(_.pick(contact, 'name')) });  app.get('/api/contacts', (req, res) => {   var contactsMinimal = [];   contacts.forEach(contact => {     contactsMinimal.push(_.pick(contact, ['id', 'name']));   });   res.json(contactsMinimal); });  app.get('/api/contacts/:id', authCheck, (req, res) => {   res.json(_.find(contacts, { id: parseInt(req.params.id) })); });  app.listen(3001); console.log('Listening on http://localhost:3001');

We’ve got an array of contacts data here that is being returned in some fashion at our two endpoints. For the /api/contacts endpoint, we’re using Lodash to pick out just the id and name fields from the objects in the array. For the /api/contacts/:id endpoint, we’re looking for the contact with a specific id and returning the whole object for just that contact. For the sake of simplicity, we’re just mocking out data retrieval here. In an actual application, this data would be queried from a database.

Sign Up for Auth0

You’ll likely have noticed the authCheck that we set up near the top in our Express server. This is the middleware that is applied to the /api/contacts/:id route, and it requires some identifying information from us. Most notably, we need to give it a secret key which will be checked against the decoded JWTs that come to the API to verify validity. When we use Auth0, all we need to do is provide the middleware our secret key and client ID.

If you haven’t alreadysigned up for Auth0, head over and do that now. After you sign up, you’ll find your application’s client secret and client ID in the management area . Once you grab those keys, you can place them in the appropriate spots in the middleware, and you’ll be good to go.

Be sure to put your localhost domain and port in the "Allowed Origins" textarea so that Auth0 will allow requests from your test domain.

Build a React + Flux App with User Authentication

Set Up the Index and Routing

Let’s start by setting up our index.js file. We’ll need to modify the file that was provided by the Yeoman generator to work for our app.

// src/index.js  import 'core-js/fn/object/assign'; import React from 'react'; import ReactDOM from 'react-dom'; import { browserHistory } from 'react-router'; import Root from './Root';  // Render the main component into the dom ReactDOM.render(<Root history={browserHistory} />, document.getElementById('app'));

We’re rendering a component called Root , which is passed a prop called browserHistory , against a DOM element called app .

To finish out the routing setup, we’ll need to create a Root.js file to handle our routes.

// Root.js  import React, { Component } from 'react'; import { Router, Route, IndexRoute } from 'react-router'; import Index from './components/Index'; import ContactDetail from './components/ContactDetail';  import App from './components/App';  class Root extends Component {    // We need to provide a list of routes   // for our app, and in this case we are   // doing so from a Root component   render() {     return (       <Router history={this.props.history}>         <Route path='/' component={App}>           <IndexRoute component={Index}/>           <Route path='/contact/:id' component={ContactDetail} />         </Route>       </Router>     );   } }  export default Root;

With React Router we can wrap individual Route s within a top Router and point them to specific paths and components. The Router uses a parameter called history which is used to parse the URL bar and construct location objects. We’re passing down a prop called history from index.js for this, which we saw previously.

We should also take this opportunity to include the Lock widget. We can either install this with npm and include it in our webpack build, or we can include it as a script tag. Let’s bring it in as a script tag for now, just for the sake of simplicity.

    <!-- src/index.html -->      ...      <!-- Auth0Lock script -->     <script src="//cdn.auth0.com/js/lock-9.1.min.js"></script>      <!-- Setting the right viewport -->     <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />          ...

Set Up the App Component

The first component that we set up shoud be our top-level App component. Go ahead and rename Main.js to App.js and bring in some of the components from React Bootstrap.

 // src/components/App.js  import 'normalize.css/normalize.css'; import 'bootstrap/dist/css/bootstrap.min.css';  import React, { Component } from 'react'; import Header from './Header'; import Sidebar from './Sidebar'; import { Grid, Row, Col } from 'react-bootstrap';  class AppComponent extends Component {    componentWillMount() {     this.lock = new Auth0Lock('YOUR_AUTH0_CLIENT_ID', 'YOUR_AUTH0_DOMAIN);   }    render() {     return (       <div>         <Header lock={this.lock}></Header>         <Grid>           <Row>             <Col xs={12} md={3}>               <Sidebar />             </Col>             <Col xs={12} md={9}>               {this.props.children}             </Col>           </Row>         </Grid>       </div>     );   } }  export default AppComponent;

We’ve got some other components that we’re calling, namely Header and Sidebar . We’ll create those components next, but for now, let’s focus on what’s happening in componentWillMount . This is where we can set up our Auth0Lock instance, and to do so, we just need to call a new Auth0Lock and pass in our client ID and client domain. As a reminder, these two items can be retrieved from your Auth0 management area.

One important thing to note here is that we’re calling {this.props.children} in the second Col . This will allow us to display child routes from React Router in this area, and in this way we can have a fixed sidebar with a dynamic view in the main portion of the app.

We’re also passing down the Auth0Lock instance as a prop into Header , so let’s create that next.

Create the Header Component

Let’s put in a navbar header which will also serve as the location for some buttons for allowing users to log in and log out of the app.

 // src/components/Header.js  import React, { Component } from 'react'; import { Nav, Navbar, NavItem, Header, Brand } from 'react-bootstrap'; // import AuthActions from '../actions/AuthActions'; // import AuthStore from '../stores/AuthStore';  class HeaderComponent extends Component {    constructor() {     super();     this.state = {       authenticated: false     }     this.login = this.login.bind(this);     this.logout = this.logout.bind(this);   }    login() {     // We can call the show method from Auth0Lock,     // which is passed down as a prop, to allow     // the user to log in     this.props.lock.show((err, profile, token) => {       if (err) {         alert(err);         return;       }       this.setState({authenticated: true});     });   }    logout() {     // AuthActions.logUserOut();     this.setState({authenticated: false});   }    render() {     return (       <Navbar>         <Navbar.Header>           <Navbar.Brand>             <a href="#">React Contacts</a>           </Navbar.Brand>         </Navbar.Header>         <Nav>           <NavItem onClick={this.login}>Login</NavItem>           <NavItem onClick={this.logout}>Logout</NavItem>         </Nav>       </Navbar>     );   } }  export default HeaderComponent;

We’re admittedly skimping a bit on the authentication details for the time being since we don’t yet have our actions and stores set up; however, this is fine to see how things work for now. The login method is where we are popping up the Lock widget and this is controlled by the "Login" NavItem . For now we are simply setting the authenticated state to be either true or false , but we’ll later see how this can be determined by the presence of a JWT for the user.

Before we can see something on the screen we need to put in our Sidebar and Index components.

Create the Sidebar and Index Components

// src/components/Sidebar.js  import React, { Component } from 'react';  class SidebarComponent extends Component {   render() {     return (       <h1>Hello from the sidebar</h1>     );   } }  export default SidebarComponent;

This will eventually be the spot where we render our list of contacts that comes back from the server, but for now we just display a simple message.

We also have an Index component which is being used by the router as the IndexRoute . We can simply have this component show a message about clicking on a contact to view their profile.

// src/components/Index.js  import React, { Component } from 'react';  class IndexComponent extends Component {    constructor() {     super();   }   render() {     return (       <h2>Click on a contact to view their profile</h2>     );   } }  export default IndexComponent;

We’re now ready to view our app for the first time! But first, let’s quickly remove or comment out a couple things in Root.js . We don’t yet have a ConactDetail component, so let’s temporarily remove the import and Route for this component.

If everything worked out, we should see the app rendered.

Build a React + Flux App with User Authentication

We should also see the Lock widget pop up when we click Login .

Build a React + Flux App with User Authentication

Implementing Flux

Flux is great for state management, but one of its downsides is that it requires a lot of code, which means this section will be a little lengthy. To keep it as trim as possible, we won’t go into too much detail on what Flux is and how it works, but you can readKen’s article on the subject for a thorough overview.

To be brief about Flux, it’s an architecture that helps us handle unidirectional data flow in our apps. Having an architecture where data can only flow in one direction is important as applications become large, because it’s much easier to reason about things if we can avoid the tangled mess of two-way data flow.

To make this happen, Flux relies on actions , a dispatcher , and stores .

Create the Dispatcher

Let’s start by creating our dispatcher.

// src/dispatcher/AppDispatcher.js  import { Dispatcher } from 'flux';  const AppDispatcher = new Dispatcher();  export default AppDispatcher;

There is only ever one dispatcher for a given React + Flux application, and it’s created by simply calling new Dispatcher() .

Create the Actions

Next, let’s put in the actions for retrieving our contacts data from the API.

// src/actions/ContactActions.js  import AppDispatcher from '../dispatcher/AppDispatcher'; import ContactConstants from '../constants/ContactConstants'; import ContactsAPI from '../utils/ContactsAPI';  export default {    recieveContacts: () => {     ContactsAPI       .getContacts('http://localhost:3001/api/contacts')       .then(contacts => {         AppDispatcher.dispatch({           actionType: ContactConstants.RECIEVE_CONTACTS,           contacts: contacts         });       })       .catch(message => {         AppDispatcher.dispatch({           actionType: ContactConstants.RECIEVE_CONTACTS_ERROR,           message: message         });       });   },    getContact: (id) => {     ContactsAPI       .getContact('http://localhost:3001/api/contacts/' + id)       .then(contact => {         AppDispatcher.dispatch({           actionType: ContactConstants.RECIEVE_CONTACT,           contact: contact         });       })       .catch(message => {         AppDispatcher.dispatch({           actionType: ContactConstants.RECIEVE_CONTACT_ERROR,           message: message         });       });   }  }

In a Flux architecture, actions need to dispatch an action type and a payload. The payload is normally the data associated with the action, and this data is picked up by the associated store to be dealt with down the line.

There are differing opinions on whether or not actions are a good place to be doing things like API calls, and some feel that anything like this should be reserved for stores. Ultimately, the setup you choose comes down to whatever works well for your app, and putting API calls in the actions provides us a good way to work with remote data.

We’re relying on a ContactsAPI and ContactConstants which we have yet to create, so let’s do that now.

Create the Contact Constants

// src/constants/ContactConstants.js  import keyMirror from 'keymirror';  export default keyMirror({   RECIEVE_CONTACT: null,   RECIEVE_CONTACTS: null,   RECIEVE_CONTACT_ERROR: null,   RECIEVE_CONTACTS_ERROR: null });

Constants give us something to identify our action types by and they are useful for syncing up our actions and stores, as we’ll see later. We’re using keyMirror to make the values of our constants match the keys themselves.

Create the Contacts API

We’ve already got a sense of how our ContactsAPI should look from our ContactActions . We want to expose some methods for sending XHR requests to the server to retrieve the data and return a Promise to handle the result. For the XHR requests themselves, we’ll use superagent which is a library that wraps XHR nicely and provides a clean way of doing HTTP requests.

// src/utils/ContactsAPI.js  import request from 'superagent/lib/client';  export default {    // We want to get a list of all the contacts   // from the API. This list contains reduced info   // and will be be used in the sidebar   getContacts: (url) => {     return new Promise((resolve, reject) => {       request         .get(url)         .end((err, response) => {           if (err) reject(err);           resolve(JSON.parse(response.text));         })     });   },    getContact: (url) => {     return new Promise((resolve, reject) => {       request         .get(url)         .end((err, response) => {           if (err) reject(err);           resolve(JSON.parse(response.text));         })     });   } }

With superagent, we can call methods like get for a GET request. In the end method we have callback that either gives us an error or a response, and we can do whatever we want with these.

If we encounter any errors in our requests, we reject right away with the error. This rejection is picked up in the catch method in our actions. Otherwise, we resolve with the data from the API.

Create the Contact Store

Before we can render any of the contacts data to the screen, we need to create the store that it will use.

// src/stores/ContactStore.js  import AppDispatcher from '../dispatcher/AppDispatcher'; import ContactConstants from '../constants/ContactConstants'; import { EventEmitter } from 'events';  const CHANGE_EVENT = 'change';  let _contacts = []; let _contact = {};  function setContacts(contacts) {   _contacts = contacts; }  function setContact(contact) {   _contact = contact; }  class ContactStoreClass extends EventEmitter {    emitChange() {     this.emit(CHANGE_EVENT);   }    addChangeListener(callback) {     this.on(CHANGE_EVENT, callback)   }    removeChangeListener(callback) {     this.removeListener(CHANGE_EVENT, callback)   }    getContacts() {     return _contacts;   }    getContact() {     return _contact;   }  }  const ContactStore = new ContactStoreClass();  // Here we register a callback for the dispatcher // and look for our various action types so we can // respond appropriately ContactStore.dispatchToken = AppDispatcher.register(action => {    switch(action.actionType) {     case ContactConstants.RECIEVE_CONTACTS:       setContacts(action.contacts);       // We need to call emitChange so the event listener       // knows that a change has been made       ContactStore.emitChange();       break      case ContactConstants.RECIEVE_CONTACT:       setContact(action.contact);       ContactStore.emitChange();       break      case ContactConstants.RECIEVE_CONTACT_ERROR:       alert(action.message);       ContactStore.emitChange();       break      case ContactConstants.RECIEVE_CONTACTS_ERROR:       alert(action.message);       ContactStore.emitChange();       break      default:   }  });  export default ContactStore;

As most stores do, we’re registering a switch statement on the AppDispatcher so we can respond to the various actions that are dispatched in the app. When the RECIEVE_CONTACTS action is dispatched, it means that we’re getting some contacts data from the API, and in this case we want to set the contacts onto an array. This is done in the setContacts function, after which we tell the EventListener to emit a change so that the app can know a change has been made.

We’ve also got logic in place for getting either a single contact or the full list of them, and these methods will be used in our components.

Before we can view our contacts, we need to create a couple components to deal specifically with our list.

Set Up the Contacts Component

The Contacts component will be used in the sidebar to display a list of our contacts. We’ll set up a Link in the list so we can later show more detail on them.

// src/components/Contacts.js  import React, { Component } from 'react'; import { ListGroup } from 'react-bootstrap'; // import { Link } from 'react-router'; import ContactActions from '../actions/ContactActions'; import ContactStore from '../stores/ContactStore'; import ContactListItem from './ContactListItem';  // We'll use this function to get a contact // list item for each of the contacts in our list function getContactListItem(contact) {   return (     <ContactListItem       key={contact.id}       contact={contact}     />   ); } class ContactsComponent extends Component {    constructor() {     super();     // For our initial state, we just want     // an empty array of contacts     this.state = {       contacts: []     }     // We need to bind this to onChange so we can have     // the proper this reference inside the method     this.onChange = this.onChange.bind(this);   }    componentWillMount() {     ContactStore.addChangeListener(this.onChange);   }    componentDidMount() {     ContactActions.recieveContacts();   }    componentWillUnmount() {     ContactStore.removeChangeListener(this.onChange);   }    onChange() {     this.setState({       contacts: ContactStore.getContacts()     });   }    render() {     let contactListItems;     if (this.state.contacts) {       // Map over the contacts and get an element for each of them       contactListItems = this.state.contacts.map(contact => getContactListItem(contact));     }     return (       <div>         <ListGroup>           {contactListItems}         </ListGroup>       </div>     );   } }  export default ContactsComponent;

We need to start with an initial state, and when we use ES2015, this can be done using this.state in the constructor. We’re binding the onChange method to this so that we can get the proper this context in our methods. This is crucial when we go to do things like this.setState in any of our component methods.

When the component mounts, we’re asking for the initial list of contacts by calling our ContactActions.recieveContacts action directly. This will send an XHR request to the server (as we’ve specified in ContactsAPI ) and will trigger the ContactStore to handle the data. We need to add a change listener in the componentWillMount lifecycle method which uses the onChange method as its callback. The onChange method is responsible for setting the state with the current list of contacts from the store.

We’re mapping over the contacts data that is set on state and creating list items out of each of them so that we can nicely display it in a ListGroup , which is a component that comes from React Bootstrap. We need another component for this called ContactListItem , so let’s put that in now.

Create the Contact List Item Component

The ContactListItem component needs to create a ListGroupItem (another React Bootstrap component) with a React Router Link inside which will eventually take us to the listed contact’s details.

// src/components/ContactListItem.js  import React, { Component } from 'react'; import { ListGroupItem } from 'react-bootstrap'; import { Link } from 'react-router';  class ContactListItem extends Component {   render() {     const { contact } = this.props;     return (       <ListGroupItem>         <Link to={`/contact/${contact.id}`}>                     <h4>{contact.name}</h4>         </Link>       </ListGroupItem>     );   } }  export default ContactListItem;

Here we’re receiving the contact as a prop and simply rendering out the name property.

Fixing the Sidebar

Let’s make one final adjustment before we view the app, which is to fix the Sidebar so that it shows the list of contacts instead of the message we had before.

// src/components/Sidebar.js  import React, { Component } from 'react'; import Contacts from './Contacts';  class SidebarComponent extends Component {   render() {     return (       <Contacts />     );   } }  export default SidebarComponent;

With this in place, we should now be able to view our list of contacts.

Build a React + Flux App with User Authentication

Create the Contact Detail Component

One of the last pieces of the application we’ll need is the contact detail area which is meant to take up the main part of the page. When a contact name is clicked, a request should be made to the server to retrieve that contact’s details which can then be displayed.

You may have noticed when we set up our Express app that we immediately applied the JWT middleware ( authCheck ) to the /contacts/:id route, meaning that without a valid JWT, we won’t be able to access the resource. While this may or may not be a realistic scenario for your app, restricting just the user’s details to authenticated users will give us a good way of seeing how to work with parts of our app that require authentication for this demo.

We’ve already got our action and store set up to handle a single contact, so let’s get right to implementing the component.

// src/components/ContactDetail.js  import React, { Component } from 'react'; import ContactActions from '../actions/ContactActions'; import ContactStore from '../stores/ContactStore';  class ContactDetailComponent extends Component {    constructor() {     super();     this.state = {       contact: {}     }     this.onChange = this.onChange.bind(this);   }    componentWillMount() {     ContactStore.addChangeListener(this.onChange);   }    componentDidMount() {     ContactActions.getContact(this.props.params.id);   }    componentWillUnmount() {     ContactStore.removeChangeListener(this.onChange);   }    componentWillReceiveProps(nextProps) {     this.setState({       contact: ContactActions.getContact(nextProps.params.id)     });   }    onChange() {     this.setState({       contact: ContactStore.getContact(this.props.params.id)     });   }    render() {     let contact;     if (this.state.contact) {       contact = this.state.contact;     }     return (       <div>         { this.state.contact &&           <div>             <img src={contact.image} width="150" />             <h1>{contact.name}</h1>             <h3>{contact.email}</h3>           </div>         }       </div>     );   } }  export default ContactDetailComponent;

This component looks very similar to our Contacts component, but it’s now only dealing with a single contact object. Notice that we’re passing an id argument to the getContact method on ContactActions and ContactStore . This id comes from React Router and is provided by the params . The componentWillReceiveProps method is used to bring out the id from params when we move between contacts in the list, or in other words, when we want to view the "next" contact.

Bring back the Contact Detail Route

Before we can view this, we need to bring back the ContactDetail route in our Root.js file.

// src/Root.js  ...  render() {     return (       <Router history={this.props.history}>         <Route path='/' component={App}>           <IndexRoute component={Index}/>           <Route path='/contact/:id' component={ContactDetail} />         </Route>       </Router>     );   }    ...

We can now click on a contact to view their details, but we’re not yet able to get through.

Build a React + Flux App with User Authentication

The reason for this unauthorized error is that we have middleware protecting the contact detail resource on the server. We need to provide the server with a valid JWT before it will allow our requests to get through, and to make that happen, we first need to authenticate our user. Let’s finish out the authentication pieces now.

Finish Out Authentication

So what exactly happens when a user logs in with Auth0? There are a number of items that are returned in the callback, and the most important one for our concerns today is the id_token , which is a JWT. We also get a number of other items, like the user’s profile, anaccess token, arefresh token, and others.

The good news is that we’re almost all the way there with authentication already since most of the work is done for us out of the box with Auth0. All we need to do to finish out the authentication piece is provide some logic to handle the user’s profile data and JWT that get returned on a successful login.

We’ll follow Flux architecture for this and create a set of actions, constants, and a store for authentication.

Create the AuthActions

// src/actions/AuthActions.js  import AppDispatcher from '../dispatcher/AppDispatcher'; import AuthConstants from '../constants/AuthConstants';  export default {    logUserIn: (profile, token) => {     AppDispatcher.dispatch({       actionType: AuthConstants.LOGIN_USER,       profile: profile,       token: token     });   },    logUserOut: () => {     AppDispatcher.dispatch({       actionType: AuthConstants.LOGOUT_USER     });   }  }

The setup is similar to our ContactActions , except that here we’re concerned with user login and logout. In the logUserIn method, we’re dispatching the profile and the token that will come through from our Header component when we call the action.

Create the Auth Constants

We need some new constants for authentication.

// src/constants/AuthConstants.js  import keyMirror from 'keymirror';  export default keyMirror({   LOGIN_USER: null,   LOGOUT_USER: null });

Create the Auth Store

The AuthStore is where we are eventually handling the profile and JWT that come through after a successful login. So what do we need to do with them exactly? The easiest way to work with the profile and token is to save them in local storage so that they can be recalled and used later.

// src/stores/AuthStore.js  import AppDispatcher from '../dispatcher/AppDispatcher'; import AuthConstants from '../constants/AuthConstants'; import { EventEmitter } from 'events';  const CHANGE_EVENT = 'change';  function setUser(profile, token) {   if (!localStorage.getItem('id_token')) {     localStorage.setItem('profile', JSON.stringify(profile));     localStorage.setItem('id_token', token);   } }  function removeUser() {   localStorage.removeItem('profile');   localStorage.removeItem('id_token'); }  class AuthStoreClass extends EventEmitter {   emitChange() {     this.emit(CHANGE_EVENT);   }    addChangeListener(callback) {     this.on(CHANGE_EVENT, callback)   }    removeChangeListener(callback) {     this.removeListener(CHANGE_EVENT, callback)   }    isAuthenticated() {     if (localStorage.getItem('id_token')) {       return true;     }     return false;   }    getUser() {     return localStorage.getItem('profile');   }    getJwt() {     return localStorage.getItem('id_token');   } }  const AuthStore = new AuthStoreClass();  // Here we register a callback for the dispatcher // and look for our various action types so we can // respond appropriately AuthStore.dispatchToken = AppDispatcher.register(action => {    switch(action.actionType) {      case AuthConstants.LOGIN_USER:       setUser(action.profile, action.token);       AuthStore.emitChange();       break      case AuthConstants.LOGOUT_USER:       removeUser();       AuthStore.emitChange();       break      default:   }  });  export default AuthStore;

The setUser function is what we use when a successful login comes through and it’s responsible for saving the profile and token in local storage. We’ve also got some utility methods that will help us in our components. Among them is an isAuthenticated method which can be used to conditionally hide and show various elements depending on whether or not the user is currently authenticated.

But let’s think about this for a second. In a traditional authentication setup, when the user successfully logs in, we get a session on the server, and this session is later used to say whether or not the user is currently authenticated. However, JWT authentication is stateless, and it works by having the server check the token it receives in a request against a secret key. No session or state necessary. This is great for allsorts of reasons, but it leaves us wondering how we can determine if a user is authenticated or not for the purposes of our front end app.

The good news is that all we really need to do is check whether a token is saved in local storage. If the token is invalid, the request is going to be rejected anyway and the user will need to log back in. We could take things a step further and check whether the token has expired or not, but for now just checking for the presence of a JWT is fine.

Fix the Header Component

Let’s quicky modify the header component so that it uses the AuthActions and AuthStore to actually dispatch the proper actions.

// src/components/Header.js  ...  import AuthActions from '../actions/AuthActions'; import AuthStore from '../stores/AuthStore';  class HeaderComponent extends Component {    ...    login() {     this.props.lock.show((err, profile, token) => {       if (err) {         alert(err);         return;       }       AuthActions.logUserIn(profile, token);       this.setState({authenticated: true});     });   }    logout() {     AuthActions.logUserOut();     this.setState({authenticated: false});   }    ...

With all of these changes in place, we can now log in and have the user’s profile and JWT saved.

Build a React + Flux App with User Authentication

Make an Authenticated Request

So our contact detail resource is protected by JWT authentication, and now we have a valid JWT for our user. Now all we need to do is attach it as an Authorization header when we make a request. With superagent, it’s easy to set this up in the request.

// src/utils/ContactsAPI.js  import AuthStore from '../stores/AuthStore';  ...    getContact: (url) => {     return new Promise((resolve, reject) => {       request         .get(url)         .set('Authorization', 'Bearer ' + AuthStore.getJwt())         .end((err, response) => {           if (err) reject(err);           resolve(JSON.parse(response.text));         })     });   } }

We setup the Authorization header with the Bearer scheme and get our JWT from the store to be attached. With this in place, we should now be able to access the protected content.

Build a React + Flux App with User Authentication

Final Touches: Conditionally Show and Hide Elements

We’re pretty much there with our app! To finish things out, let’s conditionally show and hide some of the elements. We’re going to want to show the "Login" nav item when the user isn’t authenticated, and hide it when they are. The opposite is true for the "Logout" nav item.

// src/components/Header.js  ...  constructor() {     super();     this.state = {       authenticated: AuthStore.isAuthenticated()     }     ...   }    ...    render() {     return (       <Navbar>         <Navbar.Header>           <Navbar.Brand>             <a href="#">React Contacts</a>           </Navbar.Brand>         </Navbar.Header>         <Nav>           { !this.state.authenticated ? (             <NavItem onClick={this.login}>Login</NavItem>           ) : (             <NavItem onClick={this.logout}>Logout</NavItem>           )}         </Nav>       </Navbar>     );   }    ...

Here we are getting the user’s authentication state from the store as the component loads up. We then use this authenticated state to conditionally show and hide the NavItem s.

We can do something similar for the message in our Index component.

// src/components/Index.js  ...  constructor() {     super();     this.state = {       authenticated: AuthStore.isAuthenticated()     }   }   render() {     return (       <div>         { !this.state.authenticated ? (           <h2>Log in to view contact details</h2>         ) : (           <h2>Click on a contact to view their profile</h2>         )}       </div>     );   }  ...

Wrapping Up

If you’ve followed along, you’ve now got a React + Flux app that calls an API and has user authentication implemented with Auth0. Great job!

There’s no doubt about it: a ton of code is involved with creating a React + Flux app and it can be hard to see the benefits of it when building small projects. However, the unidirectional data flow and app structure that Flux allows becomes really important as applications grow. Removing the mess of two-way data flow is essential for being able to reason about your app as it gets larger and larger.

Fortunately, the authentication part–which can often be tricky–is easy with Auth0. If you’re using a backend other than Node for your app, be sure to check out the Auth0 SDK that applies to you. There are integrations almost all popular languages and frameworks, including:

转载本站任何文章请注明:转载至神刀安全网,谢谢神刀安全网 » Build a React + Flux App with User Authentication

分享到:更多 ()

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址