Over the last two posts, I’ve delved into React and built my own Flux-Light Architecture, all the while trying to implement the most basic of tutorials – a two-page application with client-side routing and remote data access. It’s now time to turn my attention to authentication. I’m – as ever – going to use my favorite authentication service, Auth0. Let’s first of all get authentication working, then work on how to use it.
New Actions
I need two new actions – one for logging in and one for logging out – to support authentication. This is defined in actions.js like this:
static login(token, profile) { dispatcher.dispatch('LOGIN', { authToken: token, authProfile: profile }); } static logout() { dispatcher.dispatch('LOGOUT'); }
The Auth0 system returns a JSON Web Token and a profile object when you log in to it. These are passed along for storage into the store.
Store Adjustments
I’ve created a pair of new actions that carry data, so I need somewhere to store them. That’s done in the stores/AppStore.js file. First off, I need to initialize the data within the constructor:
constructor() { super('AppStore'); this.logger.debug('Initializing AppStore'); this.initialize('pages', [ { name: 'welcome', title: 'Welcome', nav: true, auth: false, default: true }, { name: 'flickr', title: 'Flickr', nav: true, auth: false }, { name: 'spells', title: 'Spells', nav: true, auth: true } ]); this.initialize('route', this.getNavigationRoute(window.location.hash.substr(1))); this.initialize('images', []); this.initialize('lastFlickrRequest', 0); this.initialize('authToken', null); this.initialize('authProfile', null); }
I also need to process the two actions – this is done in the onAction() method:
case 'LOGIN': if (this.get('authToken') != null) { this.logger.error('Received LOGIN action, but already logged in'); return; } if (data.authToken == null || data.authProfile == null) { this.logger.errro('Received LOGIN action with null in the data'); return; } this.logger.info(`Logging in with token=${data.authToken}`); this.set('authToken', data.authToken, true); this.set('authProfile', data.authProfile, true); this.changeStore(); break; case 'LOGOUT': if (this.get('authToken') == null) { this.logger.error('Received LOGOUT action, but not logged in'); return; } this.logger.info(`Logging out`); this.set('authToken', null, true); this.set('authProfile', null, true); this.changeStore(); break;
Both action processors take care to ensure they are receiving the right data and that the store is in the appropriate state for the action before executing it.
The UI
There were three places I needed work. The first was in the NavBar.jsx file to bring in a NavToolbar.jsx component:
import React from 'react'; import NavBrand from './NavBrand.jsx'; import NavLinks from './NavLinks.jsx'; import NavToolbar from './NavToolbar.jsx'; class NavBar extends React.Component { render() { return ( <header> <div className="_navbar"> <NavBrand/> </div> <div className="_navbar _navbar_grow"> <NavLinks pages={this.props.pages} route={this.props.route}/> </div> <div className="_navbar"> <NavToolbar/> </div> </header> ); } }
The second was the Client/views/NavToolbar.jsx component – a new component that provides a toolbar on the right side of the navbar:
import React from 'react'; import Authenticator from './Authenticator.jsx'; class NavToolbar extends React.Component { render() { return ( <div className="_navtoolbar"> <ul> <li><Authenticator/></li> </ul> </div> ); } } export default NavToolbar;
Finally, I needed the Client/views/Authenticator.jsx component. This is a Controller-View style component. I’m using the Auth0Lock library, which can be brought in through dependencies in package.json:
"dependencies": { "auth0-lock": "^7.6.2", "jquery": "^2.1.4", "lodash": "^3.10.0", "react": "^0.13.3" },
You should also add brfs, ejsify and packageify to the devDependencies, per the Auth0 documentation. Here is the top of the Client/views/Authenticator.jsx file:
import React from 'react'; import Auth0Lock from 'auth0-lock'; import Logger from '../lib/Logger'; import Actions from '../actions'; import appStore from '../stores/AppStore'; class Authenticator extends React.Component { constructor(props) { super(props); this.state = { token: null }; this.logger = new Logger('Authenticator'); } componentWillMount() { this.lock = new Auth0Lock('YOUR-CLIENT-ID', 'YOUR-DOMAIN.auth0.com'); this.appStoreId = appStore.registerView(() => { this.updateState(); }); this.updateState(); } componentWillUnmount() { appStore.deregisterView(this.appStoreId); } updateState() { this.setState({ token: appStore.get('authToken') }); }
I don’t like having the client ID and domain embedded in the file, so I’m going to introduce a local WebAPI to solve that. Ensure you swap in your own Auth0 settings here. Other than that minor change, this is the basic Controller-View pattern. Now for the rendering:
onClick() { if (this.state.token != null) { Actions.logout(); // Generate the logout action - we will be refreshed return; } this.lock.show((err, profile, token) => { this.lock.hide(); if (err) { this.logger.error(`Error in Authentication: `, err); return; } Actions.login(token, profile); }); } render() { let icon = (this.state.token == null) ? 'fa fa-sign-in' : 'fa fa-sign-out'; let handler = event => { return this.onClick(event); }; return ( <span className="_authenticator" onClick={handler}> <i className={icon}></i> </span> ); }
The render() method registers a click handler (the onClick() method) and then sets the icon that is displayed based on whether the current state is signed in or signed out. The onClick() method above it handles showing the lock. Once the response is received from the Auth0 system, I initiate an action to log the user in. If the user was logged in, the click initiates the logout action.
There is a methodology (redirect mode in Auth0 lock) that allows you to show the lock, then the page will be refreshed with a new hash containing the token. You can then store the token and restore the original page. That is all sorts of ugly to implement and follow. I like this version for it’s simplicity. I store the state and values of the authentication in the store, use actions to store that data, but don’t refresh the page.
Checking Authentication
I have a page within this app right now that requires authentication called spells. It never gets displayed because the code in NavLinks.jsx has logic to prevent it. Let’s fix that now.
First, NavLinks.jsx needs a new boolean property called authenticated:
NavLinks.propTypes = { authenticated: React.PropTypes.bool.isRequired, pages: React.PropTypes.arrayOf( React.PropTypes.shape({ auth: React.PropTypes.bool, nav: React.PropTypes.bool, name: React.PropTypes.string.isRequired, title: React.PropTypes.string.isRequired }) ).isRequired, route: React.PropTypes.string.isRequired };
I can also change the logic within the visibleLinks to check the authenticated property:
let visibleLinks = this.props.pages.filter(page => { if (this.props.authenticated === true) { return (page.nav === true); } else { return (page.nav === true && page.auth === false); } });
Now, I need to ensure that the NavBar and the AppView bubble the authentication state down the tree of components. That means adding the authenticated property to NavBar (I’ll leave that to you – it’s in the repository) and including it in the NavLinks call:
<NavLinks pages={this.props.pages} route={this.props.route} authenticated={this.props.authenticated}/>
That also means, AppView.jsx must provide it to the NavBar. This is a little more extensive. First of all, I’ve updated the state in the constructor to include an authenticated property:
this.state = { pages: [], route: 'welcome', authenticated: false };
That means updateState() must be updated to account for the new state variable:
updateState() { let token = appStore.get('authToken'); this.setState({ route: appStore.get('route'), pages: appStore.get('pages'), authenticated: token != null }); }
Finally, I can push this state down to the NavBar:
return ( <div id="pagehost"> <NavBar pages={this.state.pages} route={this.state.route} authenticated={this.state.authenticated}/> <Route/> </div> );
With this code, the Spells link will only appear when the user is authenticated.
Requesting API Data
So far, I’ve created an application that can re-jig itself based on the authentication state. But it’s all stored on the client. The authentication state is only useful if you request data from a remote server. I happen to have a Web API called /api/spells that must be used with a valid Auth0 token. You can read about it in a prior post. I’m not going to cover it here. Suffice to say, I can’t get data from that API without submitting a proper Auth0 JWT token. The code in the repository uses User Secrets to store the actual secret for the Auth0 JWT that is required to decode. If you are using the RTM version of Visual Studio 2015, right click on the project and select Manage User Secrets. Your user secrets should look something like this:
{ "JWT": { "Domain": "YOUR-DOMAIN.auth0.com", "ClientID": "YOUR-CLIENT-ID", "ClientSecret": "YOUR-CLIENT-SECRET" } }
If you run the application and browse to /api/settings, you should see the Domain and ClientID. If you browse to /api/spells, you should get a 401 response.
I can now use the same technique I used when requesting the Flickr data. Firstly, create two actions – one for the request and one for the response (in actions.js):
static requestSpellsData() { dispatcher.dispatch('REQUEST-AUTHENTICATED-API', { api: '/api/spells', callback: Actions.processSpellsData }); } static processSpellsData(data) { dispatcher.dispatch('PROCESS-SPELLS-DATA', data); }
Then, alter the Store to handle the request and response. This is a place where the request may be handled in one store and the response could be handled in a different store. I have a generic action that says “call an API with authentication”. It then sends the data to whatever action I tell it to. If I had a “SpellsStore”, the spells store could process the spells data on the return. It’s this disjoint method of handling the API call and response that allows me to have stores that don’t depend on one another. I’ve added the following to the constructor of the stores/AppStore.js:
this.initialize('spells', []);
I’ve also added the following to the case statement in onAction():
case 'REQUEST-AUTHENTICATED-API': if (this.get('authToken') == null) { this.logger.error('Received REQUEST-AUTHENTICATED-API without authentication'); return; } let token = this.get('authToken'); $.ajax({ url: data.api, dataType: 'json', headers: { 'Authorization': `Bearer ${token}` } }).done(response => { data.callback(response); }); case 'PROCESS-SPELLS-DATA': this.logger.info('Received Spells Data: ', data); this.set('spells', data);
Finally, I can adjust the views/Spells.jsx file to be converted to a Controller-View and request the data. I’ve already done this for the views/Flickr.jsx. You can check out my work on the GitHub repository.
I’ve done something similar with the Settings API. The request doesn’t require authentication, so I just process it. I also cache the results (if the settings have been received, I don’t need to ask them again). This data is stored as ‘authSettings’ in the store. I then added the authSettings to the state in the views/Authenticator.jsx component. I also need to trigger the settings grab – I do this in the views/Authenticator.jsx component via the componentWillMount() method:
componentWillMount() { if (this.lock == null && this.state.settings != null) { this.lock = new Auth0Lock(this.state.settings.ClientID, this.state.settings.Domain); } else { Actions.requestSettingsData(); } this.appStoreId = appStore.registerView(() => { this.updateState(); }); this.updateState(); }
I don’t want the authenticator to be clickable until the settings have been rendered, so I added the following to the top of the render() method:
render() { // Additional code for the spinner while the settings are loaded if (this.state.settings == null) { return ( <span className="_authenticator"> <i className="fa fa-spinner fa-pulse"></i> </span> ); }
This puts a spinner in the place of the login/logout icon until the settings are received.
Wrap-up
One of the biggest differences between MVC and Flux is the data flow. In the MVC architecture, you have a Datastore object that issues the requests to the backend and somehow updates the model, that then informs the controller via a callback (since it’s async). It feels hacky. MVC really works well when the controller can just do the request to get the model from the data store and get the response back to feed the view. Flux feels right in the async front-end development world – much more so than the MVC model.
The Flux architecture provides for a better flow of data and allows the easy integration of caching algorithms that are right for the environment. If you want to cache across restarts of the application (aka page refreshes), then you can store the data into localStore. If you want to specify a server-side refresh (for example, for alerting), then you can integrate SignalR into the system and let SignalR generate the action.
As you can guess, I’m loving the Flux architecture. After getting my head around the flow of data, it became very easy to understand. Code you understand is much easier to debug.
As always, you can get my code on my GitHub Repository.
Filed under: Web Development Tagged: auth0, ecmascript6, es2015, es6, flux, react
