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 themodel
:{{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();
}
}