Building User Registration with Ember Simple Auth

Not too long ago I used the Ember Simple Auth library to set up client-side user authorization on an Ember app that connected to a Rails API back-end. This set up was largely facilitated by this excellent blog post from Romulo Machado

Building out a simple user authorization flow, however, doesn't allow us to register, or sign up, users to our app. Luckily, with a few tweaks we can use the same Ember Simple Auth code we use to sign in a user, to sign them up.

Let's do it!

First, let's run through a quick refresher of how to implement user sign in with Ember Simple Auth

User Authorization with Ember Simple Auth

This post assumes that our back-end API (in this case, our Rails app), has a User model with a Sessions Controller for handling user sign in.

Our API will use a token-based authorization system to authenticate users.

What does that mean? Let's break it down, step by step.

  • We will design our User model such that, when a new user is created, a super secret, super unique authentication token will be generated and persisted to that user.
  • Then, when the user visits our Ember app to sign in, they will submit the log in form which will send their email and password to the API.
  • Our API authorizes the user using the email and password and will send a special packet of information back to Ember. This payload will include that user's super secret, super unique authorization token.
  • Ember, with the help of the Simple Auth library, will then store that token in the Ember session store.
  • Any subsequent requests sent from Ember to our API while that user is logged in will include this token in the request headers. Our API's current_user method will look up the current user using this token.
  • When a user "logs out" of our Ember app, we (with the help of Ember Simple Auth), will simply remove their authorization token from the session store. Subsequent requests to the API will not include the token.

Configuring Ember Simple Auth

Install the Ember Simple Auth add-on by running ember install ember-simple-auth in the terminal in the directory of our Ember app.

Then, we add the ApplicationRouteMixin to our adapter:

import ActiveModelAdapter from 'active-model-adapter';
import DataAdapterMixin from 'ember-simple-auth/mixins/data-adapter-mixin';

export default ActiveModelAdapter.extend(DataAdapterMixin, {
  namespace: 'api/v1',
  host: 'http://localhost:3000',
});

Then, we set up our log in route. In app/router.js, add the following:

import Ember from 'ember';
import config from './config/environment';

var Router = Ember.Router.extend({
  location: config.locationType
});

Router.map(function() {
  ...
  this.route('login');
});

export default Router;

Our login route object should extend the UnauthenticatedRouteMixin, which we have access to thanks to Ember Simple Auth.


import Ember from 'ember';
import UnauthenticatedRouteMixin from 'ember-simple-auth/mixins/unauthenticated-route-mixin';

export default   Ember.Route.extend(UnauthenticatedRouteMixin);

Now we can set up our authenticator in app/authenticators/devise.js

// app/authenticators/devise.js
import Devise from 'ember-simple-auth/authenticators/devise';

export default Devise.extend({
  serverTokenEndpoint: 'http://localhost:3000/users/sign_in'
});

The authenticator specifies the API endpoint to which all authorization requests are posted.

Lastly, we need to set up our authorizer:

// app/authorizers/devise.js
import Devise from 'ember-simple-auth/authorizers/devise';

export default Devise.extend({});

And tell our adapter to use this authorizer:

import ActiveModelAdapter from 'active-model-adapter';
import DataAdapterMixin from 'ember-simple-auth/mixins/data-adapter-mixin';

export default ActiveModelAdapter.extend(DataAdapterMixin, {
  namespace: 'api/v1',
  host: 'http://localhost:3000',
  authorizer: 'authorizer:devise'
});

The authenticator handles the job of building the authorization request headers, i.e. adding the user's authentication token from the session store to any requests being sent to the API.

Now that Ember Simple Auth is all set up, let's build out the sign up and sign in flow.

User Sign In

Let's start with a quick walk through of the sign in flow. Then, we'll break down how to use similar code to build out the registration flow.

First, we'll generate a login-form component with ember g component login-form.

Our form should be rendered on the app/templates/login template and should look something like this:

< form {{action "authenticate" on="submit"}}>
 
 < label for="identification">Email< /label>
  {{input value=identification placeholder="Enter Login" class="form-control"}}

  < label for="password">Password< /label>
  {{input value=password type="password" placeholder="Enter Password"}}
  
  < button type="submit">Login< /button>

< /form>

{{#if errorMessage}}
  {{errorMessage}}
{{/if}}

Our component object should look something like this:

import Ember from 'ember';

const { service } = Ember.inject;

export default Ember.Component.extend({
  session: service('session'),

  actions: {
    authenticate() {
     let { identification, password } =      
this.getProperties('identification', 'password');
     this.get('session')
       .authenticate('authenticator:devise',      
          identification, password)
       .catch((reason) => {
       this.set('errorMessage', reason.error || reason);
     });
   },
 }
});

So, when a user clicks the "Log In" button, the authenticate action will fire. This action collects the email and password from the form and passes them as arguments to the authenticate action that we are calling on the session service. This authenticate method will use the authenticator we defined to send a POST request with this email and password to our log in endpoint, http://localhost:3000/users/sign_in.

Let's take a closer look at the code behind that API end-point now.

In our Rails API, we defined the route:

Rails.application.routes.draw do
  post '/users/sign_in', to: "sessions#create
end

So, our sessions#create action needs to look something like this:

# app/controllers/sessions_controller.rb

def create
    user = User.find_by_email(params[:email])
    if user && user.authenticate(params[:password])
      if request.format.json?
        data = {
          token: user.authentication_token,
          email: user.email
        }
        render json: data, status: 201 and return
      end
    end
  end

I'm using Bcrypt for authentication on the Rails side.

Take note of the data JSON we are sending back to Ember. It includes only the token and the email of the newly signed in user. This is the data that Ember Simple Auth requires in order to then store that token in the session store.

Now that we have user sign in up and running, let's set up user registration .

User Registration

Define the Route

First, define the sign up route in your Router by adding the following to your app/router.js

this.route('signup');

Then, define your Sign Up route object such that it sets the model hook to return an empty User record. If you're familiar with Rails, think of this as setting an empty, new instance of the User class in the users#new or registrations#new action so that you can pass it to a form_for form builder.

import Ember from 'ember';
import UnauthenticatedRouteMixin from 'ember-simple-auth/mixins/unauthenticated-route-mixin';

export default Ember.Route.extend(UnauthenticatedRouteMixin, {
  model() {
    return this.store.createRecord('user')
  }
});

Define the Component

Now, generate your signup-form component and render it on the app/templates/signup template:

  • ember g component signup-form
  • On app/templates/signup, render the component and pass in the model: {{signup-form user=model}}

Our component template should look something like this:

< form {{action "submit" on="submit"}}>

< label for="name">Name
  {{input value=user.name placeholder="enter name"}}

  < label for="email">Email< /label>
  {{input value=user.email placeholder="enter email"}}

  < label for="password">Password< /label>
  {{input value=user.password type="password" placeholder="enter password"}}

  < label for="passwordConfirmation">Password Confirmation< /label>
  {{input value=user.passwordConfirmation type="password" placeholder="enter password again"}}

  < button type="submit">Login< /button>
< /form>

{{#if errorMessage}}
  {{errorMessage}}
{{/if}}

Before we code our component object, let's stop and think about what needs to happen in order for us to successfully register and log in our user.

First, we have to create and save the new user by POST-ing the new user's info to the API. Then, we have to make a subsequent request to authenticate this new user, so that our ESA authenticator can can step in to create the appropriate request and handle the special response from http://localhost:3000/users/sign_in.

In order to do the first part, create and save the new user, we will have to use Ember Data to .save() our new user. This is not something we want our component to handle. So, we'll define a Sign Up controller and pass the save action down from that controller, into our component using a controller action.

Build the Closure Action

First, generate the controller with ember g controller signup.

Give it a save action:

import Ember from 'ember';

export default Ember.Controller.extend({

  session: Ember.inject.service('session'),

  actions: {
    save(user){
      // more code coming soon!
    }
  }
  
});

Now, pass that action in to your component as a closure action:

// app/templates/signup.hbs

{{signup-form user=model triggerSave=(action "save") errorMessage=errorMessage}}

Note: We are also setting a property, errorMessage, so that we can pass in any error messages we catch when we save new users. In this way, we can show our users error messages on any failed sign up attempts.

Now we're ready to code our signup-form component:


import Ember from 'ember';

const { service } = Ember.inject;

export default Ember.Component.extend({
  session: service('session'),


  actions: {
    submit(){
      let user = this.get('user')
      this.attrs.triggerSave(user);
    }
 }
});

So, when a user submits the sign up form, the submit component action is triggered. This in turn fires the save action of our sign up controller, passing that action an argument of the user object we had previously set equal to our model.

Now let's build out the save action:

import Ember from 'ember';

export default Ember.Controller.extend({

  session: Ember.inject.service('session'),

  actions: {
    save(user){
      let newUser = user;
      newUser.save().catch((error) => {
        this.set('errorMessage', error)
      })
      .then(()=>{  
        this.get('session')
        .authenticate('authenticator:devise',    
          newUser.get('email'), newUser.get('password'))
        .catch((reason) => {
          this.set('errorMessage', reason.error ||reason);
        });
      })
    }
  } 
});

Let's break down what's going on here:

  • First, we POST the new user info to the API, with the help of Ember Data:
    • newUser.save
  • Then, on the Rails side, the create action of the User's Controller will create the new user and send a successful response back to ember:
module Api
  module V1
    class UsersController < ApplicationController
       def create
         user = User.create(user_params)
         return head :ok
       end

      private
        def user_params
          params.require(:user).permit(:name, :email,  
            :password, :password_confirmation)
        end

Note that we are sending any data back to Ember in the response body. We definitely don't want to send the user's password back over the interwebs, and Ember doesn't need any data from Rails right now, beyond the indication that we successfully created a user. Ember does need to make a subsequent authentication request, but it can do so using the email and password associated to the newUser object created within our component.

...
.then(()=>{  this.get('session')
  .authenticate('authenticator:devise',  
     newUser.get('email'), newUser.get('password'))
  .catch((reason) => {
     this.set('errorMessage', reason.error || reason);
   });
})

And that's it! Before you go, don't forget to build your log out action.

Log Out

In your Application route:

// app/routes/application.js
...
actions: {
    logout(){
      this.get("session").invalidate();
    }
  }

subscribe and never miss a post!

Blog Logo

Sophie DeBenedetto

comments powered by Disqus
comments powered by Disqus