Server-less Auth with CouchDB

Auth is hard and often gets in the way of quick prototyping. I like using CouchDB to get up and running quickly because it's easy and could become a long term solution if needed.

This is how I go about setting up a basic CouchDB auth system.

Before we start, you need to have CouchDB and Node/NPM installed on your machine. My example will be in Node 5.6.0.

You can grab the code at this repository or setup your own build system. We're going to setup a minimal dev server using Webpack Dev Server and serve up a React app. If we skip the boilerplate, the first thing we need to do is model our data. The minimal state needed for our example looks like this

{
  user: {
    name: string,
    authenticated: bool
  },
  error: string,
  routing: object,
  form: object    
}

We need to keep track of users, their names and authentication state in order to properly route them to where they need to be.

routing and form are pieces of the application's state that allow us to navigate pages and submit requests.

We need some way to keep all of this state in order, so we're going to use Redux to create reducer functions that describe our state. We will also use React-Router, React-Redux and React-Router-Redux in order to navigate between pages and connect those actions to our state object. You can find all of these packages on NPM.

We need 3 pages - Home, SignUp, and Login.

Our App directory will look like this -

├─┬ actions/
 │ └── actions.js
 ├── app.jsx
 ├── index.html
 ├─┬ components/
 │ └── Form.js
 ├─┬ containers/
 │ ├── Home.js
 │ ├── Login.js
 │ └── SignUp.js
 ├─┬ reducers/
 │ ├── reducers.js
 │ └─┬ users/
 │   └── reducers.js
 └─┬ scss/
   └── app.scss

Components

We're going to make a distinction between React components that connect to our state and ones that just get variables passed into them. We can put our stateful ones in containers and the others in components.

The only component we need is a Form. It can be reused in Login.js and SignUp.js. Those will be stateful components, along with Home.js. The state will be updated via redux actions and reducers. So let's start there.

Our form component is stateless and uses redux-form to create the reducer in charge of our app's forms.

import React, { Component, PropTypes } from 'react'  
import { reduxForm } from 'redux-form'  
import { SIGNUP_FORM_FIELDS } from '../actions/actions.js'

class Form extends Component {  
  render() {
    const {
      fields: { username, password },
      handleSubmit,
      resetForm,
      formType
    } = this.props
    return (
      <div className="signup-container">
        <form onSubmit={ handleSubmit }>
          <div className="signup-username">
            <input placeholder="Username" { ...username }/>
          </div>
          <div className="signup-password">
            <input placeholder="Password" { ...password }/>
          </div>
          <div className="signup-buttons">
          <button onClick={ handleSubmit }>
                        { formType === 'SIGN_UP' ? 'Sign Up' : 'Login' }
          </button>
          </div>
        </form>
      </div>
    )
  }
}

Form.propTypes = {  
  fields: PropTypes.object.isRequired,
  handleSubmit: PropTypes.func.isRequired,
  resetForm: PropTypes.func.isRequired,
    formType: PropTypes.string.isRequired
}

export default reduxForm({  
  form: 'form',
  SIGNUP_FORM_FIELDS
})(Form)

This component will be used by both SignUp.js and Login.js. These components are similar but it's easier if they exist separately for this example.

import React, { Component, PropTypes } from 'react'  
import { connect } from 'react-redux'  
import { reduxForm } from 'redux-form'  
import { createSelector } from 'reselect'  
import {  
    logInUser,
    submitForm,
    resetForm,
    SIGNUP_FORM_FIELDS
} from '../actions/actions.js'
import Form from '../components/Form'

class App extends Component {

  constructor(props) {
    super(props)
  }

  render() {
    const { dispatch, user, error } = this.props
    return (
      <div>
        <Form
        fields={ SIGNUP_FORM_FIELDS }
        resetForm={ field => dispatch(resetForm(field)) }
        onSubmit={ fields => dispatch(logInUser(fields)) }
                formType={'SIGN_UP'} />

            { error ? error.message : null }
      </div>
    )
  }
}

App.propTypes = {  
  user: PropTypes.object.isRequired
}

const user = state => state.user  
const error = state => state.error

export const auth = createSelector(  
  user,
  error,
  (user, error) => {
    return {
      user,
      error
    }
  }
)

export default connect(auth)(App)

The only difference in Login.js is that it's formType prop identifies that form as the login form.

Actions

In order for our components to update their state, we're going to define actions in the form of functions that return a description of the next state. At the very least, we will need actions to signup/login users, logout users, and handle any errors.

// action types
export const SIGNUP_FORM_FIELDS = ['username', 'password']  
export const ERROR = 'ERROR'

// other constants
export const AUTHENTICATION_STATE = {  
  AUTHENTICATED: 'AUTHENTICATED',
  UNAUTHENTICATED: 'UNAUTHENTICATED'
}

const { AUTHENTICATED, UNAUTHENTICATED } = AUTHENTICATION_STATE

// action creators

export function handleError(err) {  
  return { type: ERROR, err }
}

export function logOutUser(user) {  
  return { type: UNAUTHENTICATED, user }
}

export function logInUser(user) {  
  return { type: AUTHENTICATED, user }
}

Reducers

Our root reducer consumes all other reducers and builds a state object. For now, we only need 2 reducers. One to test logins/signups, and one to handler errors.

import { combineReducers } from 'redux'  
import {reducer as formReducer} from 'redux-form'  
import { routeReducer } from 'react-router-redux'  
import { createNewUser } from './users/reducers'  
import { AUTHENTICATION_STATE, ERROR } from '../actions/actions.js'

const { UNAUTHENTICATED, AUTHENTICATED } = AUTHENTICATION_STATE

const DEFAULT_USER_STATE = {  
    name: '',
    authenticated: false
}

export function createNewUser(state = DEFAULT_USER_STATE, action) {  
  switch (action.type) {
    case AUTHENTICATED:
            return Object.assign({}, state, {authenticated: true, name: action.user.username})
    case UNAUTHENTICATED:
      return Object.assign({}, state, {authenticated: false, name: ''})
    default:
      return state
  }
}

function handleAuthError(state = null, action) {  
  switch (action.type) {
    case ERROR:
      return action.err
    default:
      return state
  }
}

const todoApp = combineReducers({  
    user: createNewUser,
    error: handleAuthError,
    routing: routeReducer,
    form: formReducer
})


export default todoApp

We only need to write 2 reducers because redux-form and redux-react-router give us a reducer each to describe the piece of the state they own.

In order to test this out, we can pull it all together into a parent React component and sync our state as well as our routes.

import './index.html'  
import 'babel-polyfill'  
import 'normalize.css/normalize.css'  
import './scss/app.scss'

import React from 'react'  
import ReactDOM from 'react-dom'  
import { createStore, compose, applyMiddleware } from 'redux'  
import { Provider } from 'react-redux'  
import { Router, Route, IndexRoute, browserHistory } from 'react-router'  
import { syncHistory } from 'react-router-redux'  
import { persistState } from 'redux-devtools'  
import Home from './containers/Home'  
import Login from './containers/Login'  
import SignUp from './containers/SignUp'  
import reducer from './reducers/reducers'  
import DevTools from './containers/DevTools'

const finalCreateStore = compose(  
  DevTools.instrument(),
  persistState(window.location.href.match(/[?&]debug_session=([^&]+)\b/))
)(createStore)

let store = finalCreateStore(reducer)

class App extends React.Component {  
  render() {
    return (
      <div className="base-container">
        {this.props.children}
      </div>
    )
  }
}

let routes = {  
  path: '/',
  component: App,
  indexRoute: { component: SignUp },
  childRoutes: [
    { path: 'home', component: Home },
    { path: 'login', component: Login },
    { path: 'signup', component: SignUp }
  ]
}

ReactDOM.render((  
  <Provider store={store}>
    <div className="app-wrapper">
      <Router routes={routes} history={browserHistory} />
      <DevTools />
    </div>
  </Provider>
), document.getElementById('app'))

After our imports, we compose our state with our DevTools middleware using a Redux helper named compose. This gives us a chance to pass middleware into our data flow so that we can see all of our actions in our DevTools panel.

We create a parent component and use it as a base for our routes config. Finally, we render our app with our store.

At this point, we have a working boilerplate for our auth system. We can enter a user name and password to see that our state object is being updated with the correct state and click "Sign Up" to create a user. While this does not sync to a database now, we will tie into these actions in order to talk to CouchDB.

We should be able to hit any of our 3 routes(Home, SignUp, and Login) without having credentials of any kind. This is where CouchDB comes in.

CouchDB

We need a few more tools in order to make this work. We're going to pull in redux-thunk and pouchdb in order to create async actions to talk to CouchDB.

We're also going to pull in pouchdb-authentication by Nolan Lawson for an easy to use auth API.

We need to add some imports to our app.jsx

import thunk from 'redux-thunk'  
import { syncHistory } from 'react-router-redux'  
import PouchDB from 'pouchdb'  

At this point, we have everything we need to start using CouchDB's auth system. If you want to dive deeper into how this auth system works, check this out. All you really need to know is that it stores users in a users database that requires a username and password. CouchDB automatically hashes all passwords for you.

So in a new terminal window, run the couchdb command and leave that pane open, we should see a message telling us -

"It's now time to relax".

This means we're running Couch. This message should also tell you that CouchDB is running on http://127.0.0.1:5984/. So let's connect to that DB.

PouchDB.plugin(require('pouchdb-authentication'))  
export const db = new PouchDB('http://localhost:5984/test_db', {skipSetup: true})  

We're using pouch to pull in the auth plugin and creating a reference to our pouch instance. Also notice we are exporting it because we will need it in other files. The last thing we need to do in app.jsx is stop users from being able to reach the Home page with out being logged in. We're going to take advantage of React-Router's onEnter routing property in order to check for auth in route transition and redirect if needed.

let routes = {  
  path: '/',
  component: App,
  indexRoute: { component: SignUp },
  childRoutes: [
    { path: 'home', component: Home, onEnter: requireAuth },
    { path: 'login', component: Login },
    { path: 'signup', component: SignUp }
  ]
}

function requireAuth(nextState, replace, asyncTransition) {  
  return db.getSession(function (err, response) {
    if (err) {
      console.debug(err)
      replace('/login')
    } else if (!response.userCtx.name) {
      console.debug('No one logged in', response)
            replace('/login')
    } else {
      console.debug(response.userCtx.name, 'is logged in.')
    }
    asyncTransition()
  })
}

Our routes object now checks for CouchDB authentication when going to the home route by running the requireAuth function. If we don't get credentials, we redirect to the login page.

If you run your app now and try to hit the Home route, you will be redirected to the Login page. So let's hook up to our auth in order to Sign Up/Login.

In your actions file, we need to import Pouch and our auth module again as well as our DB instance.

import PouchDB from 'pouchdb'  
PouchDB.plugin(require('pouchdb-authentication'))  
import { db } from '../app'  

We will use the pouchdb-authentication API in the form of async action creators in order to Sign Up and Login.

We need 5 new functions

export function logInUser(user) {  
  return dispatch => {
    return db.login(user.username, user.password, function (err, response) {
      if (err) {
        dispatch(handleError(err))
      } else {
                dispatch(logInUserAction(response.name))
        dispatch(routeActions.push('/home'))
      }
    })
  }
}

export function logOutUser() {  
  return dispatch => {
    return db.logout(function (err, response) {
      if (err) {
        dispatch(handleError(err))
      } else {
        dispatch(logOutRedirect(response))
      }
    })
  }
}

export function logOutRedirect(response) {  
  return dispatch => {
    return dispatch(routeActions.push('/login'))
  }
}

export function checkAuthState() {  
  return dispatch => {
    return db.getSession(function (err, response) {
      if (err) {
        console.debug(err)
        dispatch(logOutUser(err))
      } else if (!response.userCtx.name) {
        console.log('No one logged in')
        dispatch(logOutUser(err))
      } else {
        console.log(response.userCtx.name, 'is logged in.')
        dispatch(logInUser(response))
      }
    })
  }
}

export function createNewUser(fields) {  
  return dispatch => {
    return db.signup(fields.username, fields.password, function (err, response) {
      if (err) {
        dispatch(handleError(err))
        dispatch(logOutUser(err))
      } else {
        dispatch(logInUser(fields))
      }
    })
  }
}

These actions are async, they return another action after we have asked CouchDB to signup or login, and we decide then which one we want to dispatch. We renamed our old actions to logInUserAction and logOutUserAction and call them from our new async actions. This means we don't need to change anything in our containers or reducers.

Now we're ready to run our app, sign up for an account and view our home page. If you want validation that you have created a user, you can access CouchDB through an interface called Futon. Just go to http://localhost:5984/_utils/ in your browser and view the users DB.

There are a lot of different directions you could from here, but this gives you a basic auth setup that you can use on most JS projects as a starter point.

The repo lives here, if you want the code.

Thanks for reading, if you have any questions I'm @stides303 on Twitter, and if this helped you please share it for others!