User Authentication Made Easy with Firebase

Note: This tutorial is for version 2.x of the Firebase SDK and some of its content may be outdated.

No matter what sort of application you're creating, you almost always have to allow your app's users to sign in and out. Including user authentication functionality in a project is time-consuming, often wrought with gotchas, and just plain boring. It's necessary to make your app function, but it takes time away from getting to the nitty gritty of what your app is actually designed to do.

I've been messing around a bit with Firebase recently. It's an interesting "cloud"-based noSQL data store. But the feature that really caught my eye about it is its user authentication capabilities. The authentication feature is billed as an ancillary side benefit to the main data storage functionality, but I think that Firebase makes it so simple to do user authentication for client-side web applications, that I'd even recommend using it for just that feature alone! I'd like to show you just how easy it is to make an authenticated web application using the Ember framework and Firebase.

What You Need to Begin

In this article, I will assume you have Ember CLI installed. I will also assume you have at least a basic familiarity with Ember. I'll go into detail about the code we're writing as we go, but I won't be doing an exhaustive, line-by-line break-down. The Ember documentation is pretty good, though, and can fill in any gaps I might leave. You can see a live demo of the code here. All the code for this demo is located at https://github.com/zachgarwood/ember-firebase-demo.

So, let's get started!

Setup and Configuration

Ember

Create a new Ember project and jump into the project directory:

ember new ember-firebase-demo
cd ember-firebase-demo/

Firebase

To set up Firebase to handle user authentication, go to https://firebase.com, sign up for a free account, and create an app. Go to your app's management screen by clicking the Manage App button. In the sidebar menu, select the Login & Auth tab, then check the Enable Email & Password Authentication box.
Enable Email & Password Authentication

To set up our project to use Firebase, go back to your terminal and install the EmberFire addon:

ember install emberfire

When it's done installing, you should have a new app/adapters/application.js file and a new firebase property on the ENV object in the application's config file (config/environment.js). Replace "YOUR-FIREBASE-NAME" with whatever you named your Firebase app (in this example, "ember-firebase-demo"):

//config/environment.js

module.exports = function(environment) {
  var ENV = {
//...
    environment: environment,
    contentSecurityPolicy: { 'connect-src': "'self' https://auth.firebase.com wss://*.firebaseio.com" },
    firebase: 'https://ember-firebase-demo.firebaseio.com/',
    baseUrl: '/',
//...
  }
}

Torii

We will install and use the Torii addon to manage our authentication sessions:

ember install torii

We will need to manually create an adapter so that Torii and EmberFire play nice with each other:

// app/torii-adapters/application.js

import Ember from 'ember';
import ToriiFirebaseAdapter from 'emberfire/torii-adapters/firebase';

export default ToriiFirebaseAdapter.extend({
  firebase: Ember.inject.service()
});

Here we're extending the Torii adapter that EmberFire already provides for us and injecting the Firebase service that we previously set up.

Then, once again in the app's config, we need to set the name of the session service that Torii provides:

// config/environment.js

module.exports = function(environment) {
  var ENV = {
//...
    firebase: 'https://ember-firebase-demo.firebaseio.com/',
    torii: { sessionServiceName: 'session' },
    baseUrl: '/',
//...
  }
}

Here we set it to "session." Torii will automatically inject this service into our routes and controllers.

Bootstrap and Font Awesome

In order to make our Ember Firebase Demo app look pretty, we'll install the Bootstrap and Font Awesome addons:

ember install ember-bootstrap
ember install ember-cli-font-awesome

Building the App

We've got everything installed and configured, so let's start our app:

ember serve

When you point your browser to http://localhost:4200, you should see "Welcome to Ember."

The Basic Templates

Let's remove the welcome message and give our application a navigation bar:

// app/templates/application.hbs

<nav class='navbar navbar-default'>
    <div class='container'>
        <div class='navbar-brand'>EmberFirebaseDemo</div>
    </div>
</nav>  
<div class='container'>
    {{outlet}}
</div>

Now let's create some content for the index route so we're not just staring at a mostly blank page:

// app/templates/index.hbs

<div class='page-header'>
    <h1>You must be signed in to use this app</h1>
</div>

Signing Up

The first bit of functionality we need to create is allowing new users to sign up. First, we'll need to generate a new controller:

ember generate controller sign-up

This generated an app/controllers/sign-up.js scaffolding file. Let's create a signUp action in that file:

// app/controllers/sign-up.js

import Ember from 'ember';

export default Ember.Controller.extend({
  firebase: Ember.inject.service(),
  actions: {
    signUp() {
      let controller = this;
      this.get('firebase').createUser({
        email: this.get('email') || '',
        password: this.get('password') || '',
      }, (error, data) => {
        if (error) {
          console.log(error);
        } else {
          controller.set('email', null);
          controller.set('password', null);
        }
      });
    }
  }
});

Here we inject the firebase service. Then, in the signUp action, we get the service we just injected and pass in the user's email and password, along with a callback, into its createUser method. In the callback we check to see if there were any errors. (For the sake of expediency in this demo, all of our error handling will be in the form of logging to the console.) If there were no errors, the user is now signed up, so we clear the email and password properties.

But where are the email and password values coming from? We'll have to create a form, so that the user can provide them.

Generate a new route:

ember generate route sign-up

This will add a sign-up route to app/router.js as well as create scaffolding route and template files. Let's update the template it created:

// app/templates/sign-up.hbs

<div class='page-header'>
    <h1>Sign Up</h1>
</div>
{{input
    type='text'
    class='form-control'
    value=email
    placeholder='email'
}}
{{input
    type='password'
    class='form-control'
    value=password
    placeholder='password'
}}
<button
    class='btn btn-default'
    {{action 'signUp'}}
>
    {{fa-icon 'user-plus'}} Sign Up
</button>

We created email and password fields and a button with a signUp action.

Now let's add a Sign Up link to our index template:

// app/templates/index.hbs

<div class='page-header'>
    <h1>You must be signed in to use this app</h1>
</div>
<div class='list-group'>
    {{#link-to 'sign-up' class='list-group-item'}}
        Sign Up
    {{/link-to}}
</div>

So, now we should see a Sign Up link on our index page. When we click it, we'll be taken to the sign-up route and presented with a form. When we fill out the form and press the Sign Up button, the form fields should reset if the sign up was successful. You can confirm that a user was created by going back to the Login & Auth tab of your Firebase app's management screen. At the bottom there is a Registered Users table. If you don't see your email address, try hitting refresh list.

Signing In

Now that our users can sign up, we need to let them sign in. As with the last step, we'll need to create a controller:

ember generate controller sign-in

Then we'll create the signIn action:

// app/controllers/sign-in.js

import Ember from 'ember';

export default Ember.Controller.extend({
  actions: {
    signIn(provider) {
      let controller = this;
      this.get('session').open('firebase', {
        provider: provider,
        email: this.get('email') || '',
        password: this.get('password') || '',
      }).then(() => {
        controller.set('email', null);
        controller.set('password', null);
      }, (error) => {
        console.log(error);
      });
    }
  } 
});

Here we get the session service (remember, Torii automatically injects it into our routes and controllers) and create a new Firebase session instance by passing in the action's provider parameter and the user's email and password to the open method. On success, we clear the form, and on error we log to the console.

Again, we need a form to collect this information, so let's generate the route:

ember generate route sign-in

Then we'll update the generated template. It will look remarkably similar to the Sign Up template:

// app/templates/sign-in.hbs

<div class='page-header'>
    <h1>Sign In</h1>
</div>
{{input
    type='text'
    class='form-control'
    value=email
    placeholder='email'
}}
{{input
    type='password'
    class='form-control'
    value=password
    placeholder='password'
}}
<button
    class='btn btn-default'
    {{action 'signIn' 'password'}}
>
    {{fa-icon 'sign-in'}} Sign In
</button>

The notable difference is in the button's action, specifically the additional 'password' string parameter. This corresponds to the provider parameter that we saw passed into the signIn action in the controller. This parameter denotes the authentication provider, in this case Firebase's own Email & Password Authentication provider.

Finally, let's add a Sign In option to our index template:

// app/templates/index.hbs

<div class='page-header'>
    <h1>You must be signed in to use this app</h1>
</div>
<div class='list-group'>
    {{#link-to 'sign-up' class='list-group-item'}}
        Sign Up
    {{/link-to}}
    {{#link-to 'sign-in' class='list-group-item'}}
        Sign In
    {{/link-to}}
</div>

Now you should be able to go to http://localhost:4200/sign-up to create credentials, then go to http://localhost:4200/sign-in to enter those credentials and sign in. If the sign in is successful, the form will clear and you will be an authenticated user.

The Session

We just used the Torii session service to create a new session instance with the open method. Let's see what else we can do with it.

I mentioned how Torii automatically injects the session service into our routes and controllers. So, by extension, session is available in our templates, as well. Let's update our sign in template to utilize it:

// app/templates/sign-in.hbs

//...
{{#if session.isWorking}}
    <button class='btn btn-default' disabled='disabled'>
        {{fa-icon 'spinner' spin=true}} Signing in...
    </button>
{{else}}
    <button
        class='btn btn-default'
        {{action 'signIn' 'password'}}
    >
        {{fa-icon 'sign-in'}} Sign In
    </button>
{{/if}}

We check the isWorking property of the session service to see if Torii is currently in the process of authenticating a user. If so, we display a disabled Signing In button with a spinner. If not, we display the regular Sign In button.

So now, when we go to http://localhost:4200/sign-in, enter our credentials, and sign in we should see the button change as we wait to be authenticated.

"But wait!" an astute observer might exclaim, "didn't we previously sign in? Shouldn't we still be authenticated?" That's correct. But since the page has refreshed and our app reloaded since then, it has lost track of our session.

So, let's add some functionality to our application route so that whenever our app reloads, our application retrieves the current session:

// app/routes/application.js

import Ember from 'ember';

export default Ember.Route.extend({
  beforeModel() {
    this.get('session').fetch().catch((error) => {
      console.log(error);
    });
  }
});

We've added a beforeModel hook that is executed as the application route is initializing, ie. before any of the other routes. It simply calls fetch on the session service to retrieve the current user's session, if it exists.

Signing Out

Let's allow our users to sign out from anywhere inside our app. To do that we'll first need to create an application controller with a signOut action:

// app/controllers/application.js

import Ember from 'ember';

export default Ember.Controller.extend({
  actions: {
    signOut() {
      this.get('session').close();
    }
  }
});

We simply call close on the session service.

Now let's add a Sign Out button to our application template:

// app/templates/application.hbs

//...  
    <div class='container'>
        <div class='navbar-brand'>EmberFirebaseDemo</div>
        <div class='navbar-right'>
            {{#if session.isAuthenticated}}
                <button
                    class='btn btn-default navbar-btn'
                    {{action 'signOut'}}
                >
                    {{fa-icon 'sign-out'}} Sign Out
                </button>
            {{/if}}
        </div>
    </div>
//...

We used the session service's isAuthenticated property to check to see that the user is signed in before displaying the Sign Out button. Because what's the point of giving the user the option to sign out if they're not even signed in!

Welcome Page

So, now that our users can sign up, sign in, and sign out, let's create a welcome page for users once they have signed in. First, generate the route:

ember generate route welcome

Then we update the template to display information about the authenticated user:

<div class='page-header'>
    <h1>Welcome, you are signed in!</h1>
</div>
<div class='panel panel-default'>
    <div class='panel-heading'>{{session.currentUser.email}}</div>
    <div class='panel-body'>
        <img src='{{session.currentUser.profileImageURL}}' />
    </div>
</div>

Now, if you're signed in and you go to http://localhost:4200/welcome, you should see a panels with your email address in the header and your avatar in the body.

Redirection

Depending on what actions our users take and whether or not they are signed in, we want them to be redirected to various routes of our application:

  • When they successfully sign up, they should be taken to the Sign In route
  • When they successfully sign in, they should be taken to the Welcome route
  • When they sign out, they should be taken to the index route
  • When they try to access the Sign Up and Sign In routes while authenticated, they should instead be taken to the Welcome route
  • When they try to access the Welcome route while not authenticated, they should instead be taken to the Sign In route

Action-Based Redirection

Let's start with the redirection that happens when the user completes an action.

Let's add redirection to the sign-in route in the signUp action:

// app/controllers/sign-up.js

//...
    signUp() {
      let controller = this;
      this.get('firebase').createUser({
        email: this.get('email') || '',
        password: this.get('password') || '',
      }, (error, data) => {
        if (error) {
          console.log(error);
        } else {
          controller.set('email', null);
          controller.set('password', null);
          controller.transitionToRoute('sign-in');
        }
      });
    }
//...
});

And add redirection to the welcome route in the signIn action:

// app/controllers/sign-in.js

//...
    signIn(provider) {
      let controller = this;
      this.get('session').open('firebase', {
        provider: provider,
        email: this.get('email') || '',
        password: this.get('password') || '',
      }).then(() => {
        controller.set('email', null);
        controller.set('password', null);
        controller.transitionToRoute('welcome');
      }, (error) => {
        console.log(error);
      });
    }
//...
});

And then add redirection to the / route in the signOut action:

// app/controllers/application.js

//...
    signOut() {
      this.get('session').close();
      this.transitionToRoute('/');
    }
//...

Authentication-Based Redirection

Now let's add in some redirection that occurs based on the user's authentication status.

Let's add redirection to the welcome route when an authenticated user attempts to go to the sign-up or sign-in routes; update both app/routes/sign-up.js and app/routes/sign-in.js:

// app/routes/sign-up.js and app/routes/sign-in.js

import Ember from 'ember';

export default Ember.Route.extend({
  beforeModel() {
    if (this.get('session.isAuthenticated')) {
      this.transitionTo('welcome');
    }
  }
});

Here we're checking the isAuthenticated property of the session service in the beforeModel hook, while the route is initializing. If the user is already signed in, they will be redirected to the welcome route.

Now let's add redirection to the / route when a non authenticated user attempts to navigate to the welcome route. This is going to be a little bit more indirect than our previous updates. First, we're going to add an accessDenied action to our application's route:

// app/routes/application.js
import Ember from 'ember';

export default Ember.Route.extend({
  beforeModel() {
    return this.get('session').fetch();
  },

  actions: {
    accessDenied() {
      this.transitionTo('/');
    },
  },
});

All this action does is redirect to the index route. So, how do we call this action when non authenticated users try to access routes that require authentication? Well, Torii provides us with a special route method called authenticatedRoute that we can use in our router map. If a non authenticated user navigates to an authenticated route, Torii will invoke the accessDenied action.

Let's update the welcome route to be an authenticated route:

// app/router.js

//...
Router.map(function() {
  this.route('sign-up');
  this.route('sign-in');
  this.authenticatedRoute('welcome');
});
//...

Try navigating to each route while signed in and then again when signed out. You should be redirected appropriately.

Whatever It's Actually Supposed to Do

So, now you have an Ember application with user authentication! It's a little rough around the edges and not quite production-ready, to be sure; you'll want to do some input validation, put some error handling in place, and update the project's content security policy in your config for starters. But now you can start focusing on whatever it is that your app is actually supposed to do.

Show Comments