Integrating a React Frontend with Stormpath
February 9, 2016
In a previous post, I went through how we hooked up Stormpath to our Java backend. In this post, I’ll go through how we got it working with our React front-end.
In a previous post, I explained our rationale for not using the Stormpath React SDK:
For front-end applications, Stormpath provides the Stormpath React SDK, a fairly new project that tries to do a lot of the work required when adding authentication to your app. In our project, however, we opted not to use the SDK. The SDK’s code was fairly small and simple, so we decided it would be more productive in the long run to implement a narrower set of functionalities tailored to our use-case and consistent with our app’s design.
With a REST API backend and a REST client as is our case, it is an established pattern to expect a server response with a payload in a format such as JSON, XML, etc. The Stormpath Servlet Plugin however, responds with paths to server-rendered views for some of its endpoints. This works well if you are rendering your views on the server-side. The URIs to the views returned are configurable, which didn’t make much of a difference for us because we prefer containing the view routing logic within the client. Consequently and hopefully temporarily, we ended up with a fire-and-forget model because we couldn’t rely on the response from the server, and we didn’t want the client to redirect to a server-specified URI. But I digress, the issues we faced with the client-server integration are a topic for another post.
I’ll run you through the code required to implement some basic authentication features in a React app using Stormpath. The illustration code is written in ES6, and uses the Reflux flux library. Nonetheless, the code should be easily adaptable to ES5 and/or a different flux library.
First, let’s create a login form:
// login-form.js
import React from 'react'
import Reflux from 'reflux'
import ReactMixin from 'react-mixin'
import bindAll from 'lodash/bindAll'
import LinkedStateMixin from 'react-addons-linked-state-mixin'
import Actions from './actions'
import Store from './store'
@ReactMixin.decorate(LinkedStateMixin)
@ReactMixin.decorate(Reflux.connect(Store))
class LoginForm extends React.Component {
constructor (props) {
super(props)
bindAll(this)
this.state = {
username: '',
password: ''
}
}
static get propTypes () {
// History is provided by react-router.
return {
history: React.PropTypes.object.isRequired
}
}
componentWillUpdate (nextProps, nextState) {
if (nextState.loggedIn) {
this.props.history.pushState(null, '/')
}
}
onSubmit (e) {
e.preventDefault()
const username = this.state.username
const password = this.state.password
Actions.login({username, password})
}
render () {
return (
<div>
<form className='login' onSubmit={this.onSubmit.bind(this)}>
<div className='login-fields'>
<input
type='email'
ref='email'
className='field'
placeholder='Username or Email (required)'
valueLink={this.linkState('username')}
required='required'
/>
<input
type='password'
className='field'
placeholder='Password (required)'
valueLink={this.linkState('password')}
required='required'
/>
</div>
<input type='submit' className='login-button' value='Log In'/>
</form>
<LoginInfo/>
</div>
)
}
}
export default LoginForm
The component above uses the react-mixin library to make decorators work with React ES6 components. You’ll need to use Babel 5.x or a plugin if you are on Babel 6, because the unratified decorator syntax is no longer supported.
Next we’ll create a registration form:
// registration-form.js
import React from 'react'
import Reflux from 'reflux'
import bindAll from 'lodash/bindAll'
import ReactMixin from 'react-mixin'
import LinkedStateMixin from 'react-addons-linked-state-mixin'
import Actions from './actions'
import Store from './store'
@ReactMixin.decorate(LinkedStateMixin)
@ReactMixin.decorate(Reflux.connect(Store))
class RegistrationForm extends React.Component {
constructor (props, context) {
super(props, context)
bindAll(this)
this.state = {
givenName: '',
surname: '',
username: '',
password: '',
confirmPassword: '',
errorMessage: null
}
}
static get propTypes () {
return {
history: React.PropTypes.object.isRequired
}
}
componentWillUpdate (nextProps, nextState) {
if (nextState.newlyRegistered) {
this.props.history.pushState(null, '/')
}
}
onSubmit (e) {
e.preventDefault()
const givenName = this.state.givenName
const surname = this.state.surname
const email = this.state.email
const password = this.state.password
const confirmPassword = this.state.confirmPassword
Actions.register({
givenName,
surname,
email,
password,
confirmPassword
})
}
onGivenNameChanged (e) {
this.state.givenName = e.target.value
}
onSurnameChanged (e) {
this.state.surname = e.target.value
}
onEmailChanged (e) {
this.state.email = e.target.value
}
onPasswordChanged (e) {
this.state.password = e.target.value
}
onConfirmPasswordChanged (e) {
this.state.confirmPassword = e.target.value
if (this.state.password !== this.state.confirmPassword) {
this.setState({errorMessage: 'Passwords don't match.'})
} else {
this.setState({errorMessage: null})
}
}
render () {
return (
<div>
<form className='login' onSubmit={this.onSubmit.bind(this)}>
<div className='login-fields'>
<input
type='text'
ref='givenName'
className='field'
placeholder='Given Name (required)'
onChange={this.onGivenNameChanged.bind(this)}
required='required'
/>
<input
type='text'
className='field'
placeholder='Surname (required)'
onChange={this.onSurnameChanged.bind(this)}
required='required'
/>
<input
type='email'
className='field'
placeholder='Email (required)'
onChange={this.onEmailChanged.bind(this)}
required='required'
/>
<input
type='password'
className='field'
placeholder='Password (required)'
onChange={this.onPasswordChanged.bind(this)}
required='required'
/>
<input
type='password'
className='field'
placeholder='Confirm Password (required)'
onChange={this.onConfirmPasswordChanged.bind(this)}
required='required'
/>
</div>
{
this.state.errorMessage !== null
? <p style={{color: 'red'}}>{this.state.errorMessage}</p>
: null
}
<input type='submit' className='login-button' value='Create Account'/>
</div>
)
}
}
export default RegistrationForm
Next, we create the actions that get fired when forms are submitted:
// actions.js
import Reflux from 'reflux'
import { login, register } from './endpoints'
const Actions = Reflux.createActions({
login: {children: ['completed', 'failed']},
register: {children: ['completed']},
})
Actions.login.listen(function (credentials) {
return login(credentials).then(this.completed).catch(this.failed)
})
Actions.register.listen(function (credentials) {
this.progressed()
// Register returns a view, not a JSON response.
return register(credentials).then(this.completed).catch(this.completed)
})
export default Actions
We use the Fetch API to make calls to our API using isomorphic fetch, but you can easily replace it with XHR or any of its wrapper libraries:
// endpoints.js
import fetch from 'isomorphic-fetch'
const API_URL = 'http://localhost:8080/api'
export function login (credentials) {
return fetch(`${API_URL}/oauth/token`, {
method: 'post',
credentials: 'include',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: `grant_type=password&username=${credentials.username}&password=${credentials.password}`
})
}
export function register (credentials) {
return fetch(`${API_URL}/register`, {
method: 'post',
credentials: 'include',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: `givenName=${credentials.givenName}&surname=${credentials.surname}&email=${credentials.email}&password=${credentials.password}&confirmPassword=${credentials.confirmPassword}`
})
}
You’ll notice that I construct the request body, as in: grant_type=password&username=${credentials.username}&password=${credentials.password}
. I do so because for reasons I’ve yet to explore, the Stormpath endpoints don’t like bodies formed using FormData, as in:
let formData = new FormData()
formData.append("username", credentials.username)
formData.append("password", credentials.password)
formData.append("grant_type", "password")
The final step is just a matter of listening for actions in your store, and triggering state changes in your components.
//store.js
import Reflux from 'reflux'
import Actions from '../actions'
const Store = Reflux.createStore({
listenables: [Actions],
onLoginCompleted: function (response) {
if (response.ok) {
this.trigger({loggedIn: true})
}
},
onRegisterCompleted: function () {
this.trigger({newlyRegistered: true})
}
})
export default Store
That’s it.
Unrelated to React and Stormpath, I ran into a number of issues while using the Fetch API, and here are my tips for saving yourself some valuable time:
- The Fetch Standard is your friend.
Credentials: “include”
is important.- Understand the difference between
cors
andno-cors
.