TL;DR: EmberJS is a JavaScript framework for building ambitious web applications. It's a framework that was built for productivity and designed with developer ergonomics in mind. Currently, EmberJS has over 15,000 stars on GitHub. In this tutorial, I'll show you how easy it is to build a web application with EmberJS 2 and add authentication to it. Check out the repo to get the code.
EmberJS was developed by Yehuda Katz. It was initially released in December 2011. EmberJS was also formerly known as SproutCore MVC framework. New applications now run on EmberJS 2 which was released in August, 2015. EmberJS 2.0 introduced new APIs, and removed deprecated ones from Ember 1. The goal of Ember 2 is to remove badly designed and unecessarily complicated code from Ember 1. And apps that run on Ember 1.13 without any deprecation warnings should run without issues on Ember 2.0. Currently, many popular products use EmberJS to build their user interfaces. Such platforms include LinkedIn, Yahoo, Zendesk, Square, PlayStation Now, Apple Music, Heroku Dashboard, Twitch, Discourse, IndieHackers and more. There is a comprehensive list of projects using Emberjs on builtwithember.io. EmberJS documentation is very detailed, and there is a vibrant community of users.
Understanding Core Concepts in EmberJS
If you have experience with frameworks like VueJS, Angular and React, then you'll understand how EmberJS works in a split second. Developers coming from the jQuery world might find it difficult to comprehend at first glance. But if you are familiar with frameworks like Laravel and Rails, then you'll discover a pattern that'll make fall in love with EmberJS.
I'll give a basic overview of these concepts to nourish your understanding of EmberJS. They are:
- Routing
- Templates
- Models
- Components
- Controllers
Routing
Let's take a good look at how a typical EmberJS app works. Our fictitious app is a Student Management System and the URL is https://studember.ng
. One of the URLs in this app is https://studember.ng/students
. This route simply returns the details of all the students registered on this app. Now, check out what happens when the user loads the app for the first time.
Ember router maps the URL to a route handler. The route handler then renders a template and loads a model that is available to the template.
import Ember from 'ember';
import config from './config/environment';
const Router = Ember.Router.extend({
location: config.locationType,
rootURL: config.rootURL
});
Router.map(function() {
this.route('students');
});
export default Router;
router
import Ember from 'ember';
export default Ember.Route.extend({
model() {
return [
'David Ajimobi',
'Olorigbeske Ojuyobo',
'Orieja Michael'
]
}
});
Route Handler
You can easily create a route using the Ember CLI's generate
command like so:
ember generate route route-name
This creates a route file at app/routes/route-name.js
, a template for the route at app/templates/route-name.hbs
, and a unit test file at tests/unit/routes/route-name-test.js
. It also adds the route to the router.
Templates
Templates are used to organize the HTML layout of the application. By default, EmberJS uses Handlebars templates. Templates can display properties provided to them from a controller or a component. The screen rendered to the user is composed of handlebars templates. A typical example is this:
<h3> {{title}} </h3>
<ul>
{{#each people as |person| }}
<li>{{person}}</li>
{{/each}}
</ul>
template.hbs
The template extension is .hbs.
Model
Models are objects that represent the underlying data that your application presents to the user. The structure and scope of your app will determine the types and number of models that will present in it.
A typical example is this:
Our student management app might have a Student
model to represent a particular student. Models are also used to persist data. Typically, most models fetch and persist data to a store. The store could be a database on a server or simply a JSON file.
import DS from 'ember-data';
export default DS.Model.extend({
first_name: DS.attr(),
last_name: DS.attr(),
city: DS.attr(),
age: DS.attr(),
});
app/models/student.js
Ember comes with a data management library called Ember Data to help deal with persistent application data. The library requires you to define the structure of the data you wish to provide to your application by extending DS.Model
. At first, using Ember Data may feel different than the way you're used to writing JavaScript applications. Many developers are familiar with using AJAX to fetch raw JSON data from an endpoint, which may appear easy at first. Over time, however, complexity leaks out into your application code, making it hard to maintain. Ember Data helps you manage your models in a simple way as your application grows.
Ember Data gives you a single store that is the central repository of models in your application. Components and routes can ask the store for models, and the store is responsible for knowing how to fetch them.
You can easily create a model using the Ember CLI's generate
command like so:
ember generate model student
This creates a model at app/models/student.js
file:
import DS from 'ember-data';
export default DS.Model.extend({
});
Components
Ember Components consist basically of two parts: a Handlebars template, and a JavaScript file that defines the component's behavior.
Components must have at least one dash in their name, e.g active-list
. This helps Ember differentiate it from native HTML tags. Components control how the user interface behaves. Components are represented in the view with the curly brace rather than the angle tag like this:
{{active-list}}
Ember provides some methods that are triggered at various points from creating a component up until the component is destroyed. This is called the Component's Lifecycle. You can declare methods to hook into the component's lifecycle to control the behaviour of components in your app.
On Initial Render, we have:
- init
- didReceiveAttrs
- willRender
- didInsertElement
- didRender
On Re-render, we have:
- didUpdateAttrs
- didReceiveAttrs
- willUpdate
- willRender
- didUpdate
- didRender
On Component Destroy, we have:
- willDestroyElement
- willClearRender
- didDestroyElement
A typical example of a component is this:
import Ember from 'ember';
export default Ember.Component.extend({
init() {
this._super(...arguments);
this.errors = [];
},
didUpdateAttrs() {
this._super(...arguments);
this.set('errors', []);
},
actions: {
required(event) {
if (!event.target.value) {
this.get('errors').pushObject({ message: `${event.target.name} is required`});
}
}
}
});
You can easily create a component using the Ember CLI's generate
command like so:
ember generate component student-list
This creates a component file at app/components/student-list.js
, a template for the component at app/templates/components/student-list.hbs
, and an integration test file at tests/integration/components/student-list-test.js
.
Controllers
Ember Controllers are routable objects meant to decorate a model with display logic. They sit between the template and model to deal with logic and properties that do not belong to the view or the model.
When you have a property that needs to be in the template but doesn't exist in the model, you can place it the controller:
import Ember from 'ember';
export default Ember.Controller.extend({
canDelete: true
});
This property can now be accessed in the template:
{{#if canDelete}}
// Go ahead and delete the student
{{/if}}
The controller can also be used to make model data more readable to the user. An example is returning the fullname of the user:
import Ember from 'ember';
export default Ember.Controller.extend({
getFullName() {
return `${this.get('model.firstName')} - ${this.get('model.lastName')}`
}
});
We can just call getFullName
in the template:
<span>{{ getFullName }} is the senior prefect.</span>
You can easily create a controller using the Ember CLI's generate
command like so:
ember generate controller students
This creates a controller file at app/controllers/students.js
, and a unit test file at tests/unit/controllers/students-test.js
.
Next, let's build an application with Emberjs 2.
Our App: Whistle Blower
The app we will build today is called Whistle Blower
. A Whistle Blower is a person who exposes any kind of information or activity that is deemed illegal, unethical, or not correct within an organization that is either private or public. The Whistle Blower app does the following:
- It gives information about whistle blowing activities in your region.
- It's a small community of whistle blowers.
- A guest user on the Whistle Blower app will only have acess to basic information about the whistle blowing activities on the landing page.
- An authenticated user will have access to whistle blowers and their profiles.
- An authenticated user will have access to whistle blower meetups/gatherings.
Build The Back-End
Let's build an API for our app. We'll quickly build the API with Node.js. The API is simple. This is what we need:
- An endpoint to serve the latest whistle blowing activities around the world -
/api/activities
. - An endpoint to serve whistle blowers and their profiles -
/api/whistleblowers
. - An endpoint to serve whistle blower meetups -
/api/meetups
. - Securing the endpoint that serves whistle blowers profiles and meetups, so that it can only be accessed by registered users.
Go ahead and fetch the Node.js backend from GitHub.
Note: We'll be securing the backend with Auth0, so make sure you have an account already or sign up for one.
Your server.js
should look like this:
'use strict';
const express = require('express');
const app = express();
const jwt = require('express-jwt');
const jwks = require('jwks-rsa');
const cors = require('cors');
const bodyParser = require('body-parser');
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
app.use(cors());
const authCheck = jwt({
secret: jwks.expressJwtSecret({
cache: true,
rateLimit: true,
jwksRequestsPerMinute: 5,
jwksUri: "https://{YOUR-AUTH0-DOMAIN}/.well-known/jwks.json"
}),
// This is the identifier we set when we created the API
audience: '{YOUR-API-AUDIENCE-ATTRIBUTE}',
issuer: "{YOUR-AUTH0-DOMAIN}",
algorithms: ['RS256']
});
app.get('/api/activities', (req, res) => {
let whistleBlowerActivities = [
// An array of whistleblowing activities
];
res.json(whistleBlowerActivities);
})
app.get('/api/whistleblowers', (req,res) => {
let whistleBlowers = [
// An aray of whistle blowers
];
res.json(whistleBlowers);
})
app.get('/api/meetups', (req,res) => {
let meetups = [
// An array of meetups
];
res.json(meetups);
})
app.listen(3333);
console.log('Listening on localhost:3333');
Check out the full server.js file here.
Note: YOUR-AUTH0-DOMAIN
should be replaced with your Auth0 domain.
server.js
Your package.json
file should look like this:
{
"name": "whistleblower",
"version": "0.0.1",
"description": "",
"main": "server.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "node server.js",
"dev": "nodemon server.js"
},
"author": "Auth0",
"license": "MIT",
"dependencies": {
"body-parser": "^1.15.2",
"cors": "^2.8.1",
"express": "^4.14.0",
"express-jwt": "^3.4.0",
"jwks-rsa": "^1.1.1"
}
}
Note: Make sure you have
nodemon
installed globally.
package.json
Once you have cloned the project, run an npm install
, then use postman to serve your routes like so:
API serving meetups
API serving whistle blowers
API serving whistle blowing activities
The public whistle blowing activities endpoint should be http://localhost:3333/api/activities
.
The private meetup endpoint should be http://localhost:3333/api/meetups
.
The private whistle blower endpoint should be http://localhost:3333/api/whistleblowers
.
Don't worry about the middleware in charge of securing our endpoint for now. We'll deal with that later. Now, let's build our frontend with EmberJS 2.
Build The Front-End With EmberJS 2
EmberJS has a very nice tool for scaffolding your apps. It's called the ember-cli. It's being maintained by the Ember team.
Go ahead and install the ember-cli tool globally like so:
npm install -g ember-cli
After installing globally, go ahead and scaffold a new EmberJS 2 app like so:
ember new whistleblower
Move into the new directory, whistleblower
and run ember serve
to start up your app.
Let's check out the structure of our newly scaffolded app.
whistleblower/
app/ - All the controllers, components, routes and templates reside here
config/ - All environment config files are here
node_modules/ - All the packages required for the emberjs app resides here
public/
tests/ - All the tests file resides here
vendor/
.editorconfig
.ember-cli
.eslintrc.js
.gitignore
.travis.yml
.watchmanconfig
ember-cli-build.js
package.json - File that contains the names of all the packages residing in node_modules folder
README.md
testem.js
Note: We are not writing any tests for this application. It's out of the scope of this tutorial.
We need to remove the default page content that ember presents in our app. Open app/templates/application.hbs
file and remove this:
{!-- The following component displays Ember's default welcome message. --}}
{{welcome-page}}
{{!-- Feel free to remove this! --}}
Now, our app will show a blank screen. Sweet! let's get started.
Style with Bootstrap
Go ahead and open the app/index.html
file. Here, we will add the link to the bootstrap css and js file:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>Whistleblower</title>
<meta name="description" content="">
<meta name="viewport" content="width=device-width, initial-scale=1">
{{content-for "head"}}
<link rel="stylesheet" href="{{rootURL}}assets/vendor.css">
<link rel="stylesheet" href="{{rootURL}}assets/whistleblower.css">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css">
{{content-for "head-footer"}}
</head>
<body>
{{content-for "body"}}
<script src="{{rootURL}}assets/vendor.js"></script>
<script src="{{rootURL}}assets/whistleblower.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/js/bootstrap.min.js">
{{content-for "body-footer"}}
</body>
</html>
Ember already provides a directory and a stylesheet file for your custom css. So, open app/styles/app.css
and add the css here.
That's all about our styling. Next, let's create our routes.
Create the Routes
We need users to be able to access a URL:
- That provides details about whistle blower meetups.
- That provides details about whistle blowers.
We also need a callback URL. I'll tell you why we need that later in the tutorial. Let's create these routes ASAP. The ember-cli provides generator commands that makes this easy. So go ahead and run the following commands in your terminal:
ember generate route whistle-blowers
ember generate route meetups
ember generate route callback
The route files are generated together with their respective template files. EmberJS route handler renders a template when a route is invoked.
Building the Nav Component
Let's build the Nav Component. This component will be shared amongst all the pages. Generate the component using the ember-cli:
ember generate component app-nav
This command generates a component and a template for the app nav. Open up app/templates/components/app-nav.hbs
and add this to it:
<nav class="navbar navbar-default">
<div class="navbar-header">
Whistle Blower
</div>
<ul class="nav navbar-nav navbar-right">
<li>
<button class="btn btn-danger log">Log out</button>
<button class="btn btn-info log">Log In</button>
</li>
</ul>
</nav>
Now, next we need to create an utility file for authentication and fetching API data for our routes from the backend. Well, Ember allows us to create utilities, but this is better suited for services. An Ember service is a long lived Ember object that can be made available in different parts of your application. This is exactly what we need.
Creating Services
We need to create two services, the auth and API service. The former for everything related to user authentication and the latter for fetching API data from our server. Go ahead and create both services using the ember-cli:
ember generate service auth
ember generate service whistleblowerapi
app/services/auth.js
and app/services/whistleblowerapi.js
will be created. Now, open the auth service and add this to it:
app/services/auth.js
import Ember from 'ember';
import decode from 'npm:jwt-decode';
import auth0 from 'npm:auth0-js';
const ID_TOKEN_KEY = 'id_token';
const ACCESS_TOKEN_KEY = 'access_token';
const CLIENT_ID = '{AUTH0_CLIENT_ID}';
const CLIENT_DOMAIN = '{AUTH0_DOMAIN}';
const REDIRECT = '{CALLBACK_URL}';
const SCOPE = '{SCOPE}';
const AUDIENCE = '{API IDENTIFIER}';
export default Ember.Service.extend({
auth: new auth0.WebAuth({
clientID: CLIENT_ID,
domain: CLIENT_DOMAIN
}),
login() {
this.get('auth').authorize({
responseType: 'token id_token',
redirectUri: REDIRECT,
audience: AUDIENCE,
scope: SCOPE
});
},
logout() {
this.clearIdToken();
this.clearAccessToken();
window.location.href = "/";
},
getIdToken() {
return localStorage.getItem(ID_TOKEN_KEY);
},
getAccessToken() {
return localStorage.getItem(ACCESS_TOKEN_KEY);
},
clearIdToken() {
localStorage.removeItem(ID_TOKEN_KEY);
},
clearAccessToken() {
localStorage.removeItem(ACCESS_TOKEN_KEY);
},
// Helper function that will allow us to extract the access_token and id_token
getParameterByName(name) {
let match = RegExp('[#&]' + name + '=([^&]*)').exec(window.location.hash);
return match && decodeURIComponent(match[1].replace(/\+/g, ' '));
},
// Get and store access_token in local storage
setAccessToken() {
let accessToken = this.getParameterByName('access_token');
localStorage.setItem(ACCESS_TOKEN_KEY, accessToken);
},
// Get and store id_token in local storage
setIdToken() {
let idToken = this.getParameterByName('id_token');
localStorage.setItem(ID_TOKEN_KEY, idToken);
},
isLoggedIn() {
const idToken = this.getIdToken();
return !!idToken && !this.isTokenExpired(idToken);
},
getTokenExpirationDate(encodedToken) {
const token = decode(encodedToken);
if (!token.exp) { return null; }
const date = new Date(0);
date.setUTCSeconds(token.exp);
return date;
},
isTokenExpired(token) {
const expirationDate = this.getTokenExpirationDate(token);
return expirationDate < new Date();
}
});
Go ahead and install the auth0-js
and jwt-decode
packages from the terminal:
npm install auth0-js jwt-decode --save
Our auth service contain different functions for authenticating using auth0 hosted lock,saving/extracting tokens, checking expiry date and checking if a user is logged in or not.
Note: You can fetch a property in a service using the this.get('<name-of-property>')
syntax.
Now, you might have noticed that we are importing them, using this syntax import module from npm:package
. It turns out that CommonJS(Node) module doesn't play nice with ES6 import statements in Ember. It throws an error indicating that the module can't be found. As usual, we got a work around. To get our NPM CommonJS version of our node modules to work with our ES6 import, all we have to do is:
ember install ember-browserify
and append npm
to the module name like we did in the code snippet for the auth service above.
Open the whistleblower API service and add this to it:
app/services/whistleblowerapi.js
import Ember from 'ember';
import axios from 'npm:axios';
const ACCESS_TOKEN_KEY = 'access_token';
const BASE_URL = 'http://localhost:3333';
export default Ember.Service.extend({
getMeetups() {
const url = `${BASE_URL}/api/meetups`;
return axios.get(url, { headers: { Authorization: `Bearer ${this.getAccessToken()}` }}).then(response => response.data);
},
getWhistleBlowers() {
const url = `${BASE_URL}/api/whistleblowers`;
return axios.get(url, { headers: { Authorization: `Bearer ${this.getAccessToken()}` }}).then(response => response.data);
},
getActivities() {
const url = `${BASE_URL}/api/activities`;
return axios.get(url).then(response => response.data);
},
getAccessToken() {
return localStorage.getItem(ACCESS_TOKEN_KEY);
}
});
Install the axios
module via your terminal:
npm install axios --save
Here, we fetched the meetups, whistleblowers and activities from the API. Ember already provides jQuery by default. So, an alternative is to use
Ember.$.get(url)
instead of axios. I personally love using axios, hence the reason I chose to use it here.
Ember Services are injectable. You can inject them into different parts of your application as the need arises.
Build the Routes
We created our routes earlier. Now, we need to pass data to the templates of these routes. Once a user hits a URL, they should be able to get data presented to them.
Ember provides a model
method in routes that allows us to fetch data and pass it down to the route template. So, we'll add the model method into our routes, inject the API service and call the service methods to provide data to the model hook so that it can be passed down to the templates.
Open app/routes/meetups.js
and add this:
import Ember from 'ember';
export default Ember.Route.extend({
api: Ember.inject.service('whistleblowerapi'),
model() {
return this.get('api').getMeetups();
}
});
app/routes/meetups.js
Open app/routes/whistle-blowers.js
and add this:
import Ember from 'ember';
export default Ember.Route.extend({
api: Ember.inject.service('whistleblowerapi'),
model() {
return this.get('api').getMeetups();
}
});
app/routes/whistle-blowers.js
Now, we need to create a route for our index page, which is the landing page.
ember generate route index
Open app/routes/index
and add this:
import Ember from 'ember';
export default Ember.Route.extend({
api: Ember.inject.service('whistleblowerapi'),
model() {
return this.get('api').getActivities();
}
});
app/routes/index.js
Next, we need to display data in their respective templates.
Bring Templates to Life
Open app/templates/index.hbs
and add this:
{{app-nav}}
<div class="container">
<h3 class="text-center">Whistle Blowing Updates From Across The World</h3>
</div>
<br/>
{{#each model as |update|}}
<div class="col-sm-6">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title"> {{ update.title }} </h3>
</div>
<div class="panel-body">
<p><span class="badge alert-danger"> Location: </span><strong> {{ update.location }} </strong></p>
</div>
</div>
</div>
{{/each}}
We imported the app-nav component into this template to provide navigation menu for our app.
We also looped through the model data coming from the index route using the #each
helper method.
Open app/templates/meetups.hbs
and add this:
{{app-nav}}
<div class="container">
<h3 class="text-center">Whistle Blower Meetups Across The World </h3>
</div>
<br/>
{{#each model as |meetup|}}
<div class="col-sm-6">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title"> {{ meetup.name }} </h3>
</div>
<div class="panel-body">
<p><span class="badge alert-danger"> Location: </span><strong> {{ meetup.date }} </strong></p>
</div>
</div>
</div>
{{/each}}
{{outlet}}
Open app/templates/whistle-blowers.hbs
and add this:
{{app-nav}}
<div class="container">
<h3 class="text-center">Whistle Blowers Across The World </h3>
</div>
<br/>
{{#each model as |whistleblower|}}
<div class="col-lg-2 col-sm-4">
<div class="card hovercard">
<div class="cardheader">
</div>
<div class="avatar">
<img alt="" src="{{whistleblower.avatar}}"/>
</div>
<div class="info">
<div class="title">
<a target="_blank" href="http://scripteden.com/">{{ whistleblower.name }}</a>
</div>
<div class="desc">
<p><strong>{{whistleblower.level}}</strong></p></div>
<div class="desc">
<p><span class="badge alert-danger"> Uncovered Spoils: </span> {{whistleblower.uncoveredSpoils}} </p></div>
</div>
</div>
</div>
{{/each}}
{{outlet}}
Go to your browser and check out all the routes. They should be displaying the right data:
Landing page
Meetups Route
Whistleblowers route
Next, let's add authentication to our app.
Adding Authentication to Your EmberJS 2 App
The majority of the apps we use on a daily basis have a means of authenticating users. I'll show you how to easily add authentication to our Emberjs 2 application. We'll use Auth0 as our authentication service.
Auth0 allows us to issue JSON Web Tokens (JWTs). If you don't already have an Auth0 account, sign up for a free one now.
Auth0 provides the simplest and easiest to use user interface tools to help administrators manage user identities including password resets, creating and provisioning, blocking and deleting users.
Login to your Auth0 management dashboard and let's create a new API client. If you don't already have the APIs menu item, you can enable it by going to your Account Settings and in the Advanced tab, scroll down until you see Enable APIs Section and flip the switch.
From here, click on the APIs menu item and then the Create API button. You will need to give your API a name and an identifier. The name can be anything you choose, so make it as descriptive as you want. The identifier will be used to identify your API, this field cannot be changed once set. For our example, I'll name the API Startup Battle API and for the identifier I'll set it as http://whistleblower.com. We'll leave the signing algorithm as RS256 and click on the Create API button.
Creating the Whistle Blower API
Next, let's define some scopes for our API. Scopes allow us to manage access to our API. We can define as few or as many scopes as we want. For our simple example, we'll just create a single scope that will grant users full access to the API.
Locate Scopes bar
Adding scope
Secure The Node API
We need to secure the API so that the meetups and whistle blowers endpoints will only be accessible to authenticated users. We can secure it easily with Auth0.
Open up your server.js
file and add the authCheck
middleware to the private battles endpoint like so:
app.get('/api/meetups', authCheck, (req,res) => {
let meetups = [
// Array of meetups
];
res.json(meetups);
})
app.get('/api/whistleblowers', authCheck, (req,res) => {
let whistleBlowers = [
// Array of whistle blowers
];
res.json(whistleBlowers);
})
app.listen(3333);
console.log('Listening on localhost:3333');
Try accessing the http://localhost:3333/api/meetups
endpoint again from Postman. You should be denied access like so:
Unauthorized Access
Next, let's add authentication to our front-end.
Adding Authentication to our EmberJS 2 Front-end
We created an auth
service earlier. Open up app/services/auth.js
again.
In the code present in this file, we are using an hosted version of Auth0 Lock in the login
method. We also passed in our credentials.
The auth0 node module called the Auth0's authorize
endpoint. With all the details we passed to the method, our client app will be validated and authorized to perform authentication. You can learn more about the specific values that can be passed to the authorize method here.
The parameters that you do not have yet are the {AUTH0-CLIENT-ID}
and the {CALLBACK-URL}
. When you created your API, Auth0 also created a test client which you can use. Additionally, you can use any existing Auth0 client found in Clients section of your management dashboard.
Check the Test
panel of your API from the dashboard. You'll see the test client like so:
WhistleBlower API Client
Now, go to the clients area and check for the test client. Open the client and change the Client Typefrom Non Interactive Client
to Single Page Application
.
Non interactive clients are meant to be used in machine to machine interactions. We are using an SPA to interact with the API so the client should be an SPA client. Check out Implicit Grant and client credentials exchange for more information.
Copy the CLIENT ID from the dashboard and replace it with the value of the CLIENT-ID
constant variable in your code. Replace your callback url with http://localhost:4200/callback
. Replace your client domain, scope and audience values with the domain from your Auth0 dashboard, scope from your API and API identifier respectively.
We checked whether the token has expired via the getTokenExpirationDate
and isTokenExpired
methods. The isLoggedIn
method returns true
or false
based on the presence and validity of a user id_token
.
Let's go update the app-nav
component to hide/show the login
and logout
buttons based on the user's authentication status.
Now, your app-nav
component template file should look like this:
<nav class="navbar navbar-default">
<div class="navbar-header">
{{#link-to 'index' class="navbar-brand"}} Whistle Blower {{/link-to}}
</div>
<ul class="nav navbar-nav">
<li>
{{#if loggedIn }}
{{#link-to 'meetups' class="navbar-brand"}} Meetups {{/link-to}}
{{#link-to 'whistle-blowers' class="navbar-brand"}} Whistle Blowers {{/link-to}}
{{/if}}
</li>
</ul>
<ul class="nav navbar-nav navbar-right">
<li>
{{#if loggedIn }}
<button {{ action "logout" }} class="btn btn-danger log">Log out</button>
{{else}}
<button {{ action "login" }} class="btn btn-info log">Log In</button>
{{/if}}
</li>
</ul>
</nav>
app/templates/components/app-nav.hbs
Now, we used some handlebars helpers in the code above such as #if
for conditional operations, #link-to
for linking to a route.
We have a loggedIn
variable that is set as a flag for alternating the display of the login
and logout
buttons. Now, where did that variable come from?
Open the component file responsible for the app-nav
template and add this:
import Ember from 'ember';
export default Ember.Component.extend({
auth: Ember.inject.service('auth'),
init() {
this._super(...arguments);
this.set('loggedIn', this.get('auth').isLoggedIn());
},
actions: {
login() {
this.get('auth').login();
},
logout() {
this.get('auth').logout();
}
}
});
app/components/app-nav.js
We injected the auth
service. Then we called the init
method which is a component hook that is called whenever a component is been initialized. In the init
method, we set a variable loggedIn
with the value of the user's authentication status gotten from the auth
service.
this._super(..arguments)
has to be called to override the init method that ships with Ember components.
Next, we defined the actions
. In Ember, components use actions to communicate events and changes. The methods defined in the actions object get called when the user needs to trigger an action on the UI either via buttons or any form field elements.
In our app-nav
template, we have the logout
and login
buttons that trigger some actions:
{{#if loggedIn }}
<button {{ action "logout" }} class="btn btn-danger log">Log out</button>
{{else}}
<button {{ action "login" }} class="btn btn-info log">Log In</button>
{{/if}}
Activate the Callback Route
We created a callback route earlier. This route will be called when a redirect is made from Auth0 after a successful authentication. The route will process the redirect and store the access_token
and id_token
coming from Auth0. Open app/routes/callback.js
and add this:
import Ember from 'ember';
export default Ember.Route.extend({
auth: Ember.inject.service('auth'),
beforeModel() {
this.get('auth').setAccessToken();
this.get('auth').setIdToken();
this.transitionTo('/');
},
});
app/routes/callback.js
Once a user is authenticated, Auth0 will redirect back to our application and call the /callback
route. Auth0 will also append the id_token
as well as the access_token
to this request, and our callback route will make sure to properly process and store those tokens in localStorage. If all is well, meaning we recieved an id_token
, access_token
, we will be redirected back to the /
page and will be in a logged in state.
Ember Routes provide a
beforeModel
hook that is called before the model is initialized. It can be used to conditionally prevent access to a route. It can also be used to perfom some actions like a middleware.
Add some values to Auth0 Dashboard
Just before you try to log in or sign up, head over to your Auth0 dashboard and add http://localhost:4200/callback
to the Allowed Callback URLs and http://localhost:4200
to Allowed Origins (CORS) of your client.
Secure Meetups and WhistleBlowers Routes
We need to ensure that no one can go to the browser and just type /meetups
and whistle-blowers
to access the meetups and whistleblowers routes respectively.
Open up app/routes/meetups.js
. Modify it to have the beforeModel
hook and inject the auth
service:
import Ember from 'ember';
export default Ember.Route.extend({
auth: Ember.inject.service('auth'),
api: Ember.inject.service('whistleblowerapi'),
beforeModel() {
if(!this.get('auth').isLoggedIn()) {
this.transitionTo('/');
}
},
model() {
return this.get('api').getMeetups();
}
});
app/routes/meetups.js
Open up app/routes/whistle-blowers.js
. Modify it to also have the beforeModel
hook and inject the auth
service:
import Ember from 'ember';
export default Ember.Route.extend({
auth: Ember.inject.service('auth'),
api: Ember.inject.service('whistleblowerapi'),
beforeModel() {
if(!this.get('auth').isLoggedIn()) {
this.transitionTo('/');
}
},
model() {
return this.get('api').getWhistleBlowers();
}
});
app/routes/whistle-blowers.js
In both routes, just before the model initializes, we check if the user is logged in. If the user has not been authenticated, we redirect the user back to the landing page.
Now, try to log in.
Lock Login Widget
For the first time, the user will be shown a user consent dialog that will show the scopes available. Once a user authorizes, it goes ahead to login the user and give him access based on the scopes.
User presented with an option to authorize
Note: Since we are using localhost
for our domain, once a user logs in the first time, subsequent logins will not need a user consent authorization dialog. This consent dialog will not be displayed if you are using a non-localhost domain, and the client is a first-party client.
Logged In and authorized see the private meetups page
Logged In and authorized to see the private whistleblowers page
We have successfully logged in and can access the content of both private routes. We passed an option to send an Authorization
header with a Bearer access_token
along with the GET
request in our API service. The request sends the JWT to the secured backend. The backend verifies it. If it is valid, the user is granted access to the resources.
...
getMeetups() {
const url = `${BASE_URL}/api/meetups`;
return axios.get(url, { headers: { Authorization: `Bearer ${this.getAccessToken()}` }}).then(response => response.data);
},
getWhistleBlowers() {
const url = `${BASE_URL}/api/whistleblowers`;
return axios.get(url, { headers: { Authorization: `Bearer ${this.getAccessToken()}` }}).then(response => response.data);
},
...
What happens if we don't send an access_token
? Go ahead and remove the Authorization header. Make a plain get request that doesn't send any JWT to the backend.
Aha, we get a blank page. The Chrome Dev tools screaming at us like:
401 Unauthorized
Important API Security Note: Anytime you intend using Auth0 authentication to authorize API requests, note that you'll need to use a different flow depending on your use case. Auth0 id_token
should only be used on the client-side. Access tokens should be used to authorize APIs. You can read more about making API calls with Auth0 here.
Conclusion
You have just successfully built an EmberJS 2 app and added authentication to it. To be honest, Ember has a steep learning curve. It takes a while to navigate where to find different files and put certain logic but once you get a hang of it, then it becomes a Saber for architecting and building ambitious web applications.
In addition, Auth0 can help secure your EmberJS apps with more than just username-password authentication. It provides features like multifactor auth, breached password detection, anomaly detection, enterprise federation, single sign on (SSO), and more. Sign up today so you can focus on building features unique to your app.
Auth0 offers a generous free tier to get started with modern authentication.