This is an easy and simple session lifecycle solution pattern for Ember.js 1.13/2.0 using Torii 0.6.0 that I’d like to share. Ember is a framework for building single-page web applications (SPAs) in JavaScript and HTMLBars. Torii abstracts authentication (authn) and authorization (authz) services in Ember and gives you powerful hooks into authn providers (social media login, e.g. Facebook or Twitter, or your company’s internal directory service) and customizable authz adapters (i.e. your application’s backend). It can inject the session object returned by your API into routes and controllers, and protect routes that require authorization.
There has been a lot written about Ember and Torii but I have found the literature to be out of date and/or not quite geared toward what “normal” people are trying to accomplish. Even the Torii README is difficult to follow and out of date in places. There’s also a widely-used Ember add-on called Simple Auth that leverages Torii and aims to abstract all this nonsense, but after learning how to use Torii directly, I feel that Ember Simple Auth is overkill, a little clumsy, and difficult to customize by comparison. Torii is not the be-all and end-all session management tool and store that you might be familiar with from other platforms and frameworks; it exists merely to authenticate a user, open a session against a backend, and make that session information available to other parts of the application. If you need to retrieve, manipulate, and persist other elements of your application, you’ll have to look elsewhere (i.e. Ember Data or the Ember localStorage Adapter).
This will not be a comprehensive guide to Torii. I recommend you read all the above-linked documentation and other blog posts. However, I hope it helps someone else get started with Torii and avoid the back-and-forth work and rework that I had to go through!
Prerequisites
Before we proceed, this tutorial assumes that you have some familiarity with Ember and Ember CLI. I will not be covering how to install Ember. This tutorial also assumes that you: 1. have some authn providers lined up (e.g. Facebook Login), 2. are using cookies to persist client state (either a session cookie or a JWT cookie), and 3. have a relatively stable backend API with the following endpoints:
- Session start—e.g.
POST /api/session/start
—Use the provider name (e.g. “facebook” or “facebook-oauth2”) and corresponding authorization code/token in the request body to produce a session information object (e.g. user name and email address). Set new cookie(s) in the response header. - Session info—e.g.
GET /api/session/info
—Use the cookie(s) in the request header to produce (or retrieve) the session information object. Optionally refresh the cookie(s). - Session end—e.g.
POST /api/session/end
—Use the cookie(s) in the request header to produce (or retrieve) the session information object and close the server-side session (if necessary). Delete the cookie(s).
If you’re not using cookies, that’s okay; you will just have an extra step in the adapter’s open()
method to persist e.g. the session_id or the JWT to localStorage and an extra step in the adapter’s fetch()
and close()
methods to retrieve the session_id from localStorage and send it along with the request. (There’s a lot of debate in the industry about how to secure SPAs and API resources against cross-site scripting (XSS) and cross-site request forgery (XSRF) attacks and one of the main points of contention is whether to use localStorage or cookies or some combination of both. I personally prefer stateless or state-ish APIs with an httpOnly; secure
JWT cookie, and a separate XSRF cookie to use in the double-submit pattern.)
Goals
Our objectives are straightforward. We have a handful of “public” routes that should be available to both authenticated and unauthenticated users. We also have a handful of “protected” routes, nested under a common parent route, that require an authenticated user. We want to support logging in via multiple providers and logging out (obviously). If the cookies are present and valid and the user navigates directly to a protected route, we want to take them straight there. Full browser refreshes should return the user to the previous state.
Tutorial
To create a new project and install Torii, simply run these commands:
$ ember new ember-torii-demo $ cd ember-torii-demo $ ember install torii
Next, we’ll add a torii
block to the ENV
object in config/environment.js
with our session service name (“toriiSession” for the sake of this demo, but can be anything you’d like as long as it doesn’t conflict with another service) and our providers:
// config/environment.js /* jshint node: true */ module.exports = function(environment) { var ENV = { /* ... */ torii: { sessionServiceName: 'toriiSession', providers: { 'facebook-oauth2': { apiKey: 'your-facebook-app-id', scope: 'email' } } }, contentSecurityPolicy: { // You'll want to inspect the CSP warnings in the console and add // entries here to whitelist the proper provider-specific resources. // For more information: https://github.com/rwjblue/ember-cli-content-security-policy } }; // You can customize the torii settings in each environment, e.g.: if (environment === 'development') { ENV.torii.providers['facebook-oauth2'].apiKey = 'your-facebook-test-app-id' } return ENV; };
Let’s generate our public and protected routes:
ember generate route a # home page ember generate route b # access denied page ember generate route protected ember generate route protected/c ember generate route protected/d
And simply change the routing method for the protected route(s) in app/router.js
from this.route()
to this.authenticatedRoute()
:
- this.route('protected', function() { + this.authenticatedRoute('protected', function() {
Now we can build an application route with our login()
and logout()
actions, as well as an accessDenied()
action that is called by Torii automatically if it is unable to initialize the session on open()
or fetch()
(which we’ll learn about shortly):
// app/routes/application.js import Ember from 'ember'; export default Ember.Route.extend({ actions: { login(providerName) { return this.get('toriiSession').open(providerName) .then(() => this.transitionTo('protected.c')); }, logout() { return this.get('toriiSession').close() .then(() => this.transitionTo('a')); }, accessDenied() { return this.transitionTo('b'); } } });
And our application template with our navigation links (you could put these into e.g. a Twitter Bootstrap navigation bar):
{{! app/templates/application.hbs }} <h2 id="title">Welcome to Ember</h2> {{#if toriiSession.isAuthenticated}} <p>You're logged in, {{toriiSession.user.name}}!</p> {{else if toriiSession.isWorking}} <p>Logging in...</p> {{/if}} {{link-to 'Public Page A' 'a'}}<br/> {{link-to 'Public Page B' 'b'}}<br/> {{#if toriiSession.isAuthenticated}} {{link-to 'Protected Page C' 'protected.c'}}<br/> {{link-to 'Protected Page D' 'protected.d'}}<br/> <a href="#" {{action 'logout'}}>Logout</a><br/> {{else}} <a href="#" {{action 'login' 'facebook-oauth2'}}>Login with Facebook</a><br/> {{/if}} {{outlet}}
Finally, we need to write our adapters, which act as bridges between the providers (e.g. Facebook Login) and our backend API. We haven’t strayed too far from the official Torii documentation up until this point, but now here we go! The adapters should provide three methods back to Torii: open()
, which begins a new session at login; fetch()
, which attempts to initialize an existing session; and close()
, which terminates a session. I recommend a single application adapter for the two methods common to all providers, fetch()
and close()
, and separate provider adapters for the open()
method to allow for some provider-specific logic. You can take it a step further and subclass the provider adapter off of the application adapter if you have some common logic for the open()
method as well; we’ll go with this solution because it’s the most flexible.
// app/torii-adapters/application.js import Ember from 'ember'; export default Ember.Object.extend({ providerName: null, init(providerName) { this._super(); this.providerName = providerName; }, open(postData) { return new Ember.RSVP.Promise((resolve, reject) => Ember.$.ajax({ dataType: 'json', method: 'POST', url: '/api/session/start', contentType: 'application/json', data: JSON.stringify(postData), processData: false, success: Ember.run.bind(null, resolve), error: Ember.run.bind(null, reject) })); }, fetch() { return new Ember.RSVP.Promise((resolve, reject) => Ember.$.ajax({ dataType: 'json', method: 'GET', url: '/api/session/info', success: Ember.run.bind(null, resolve), error: Ember.run.bind(null, reject) })); }, close() { return new Ember.RSVP.Promise((resolve, reject) => Ember.$.ajax({ dataType: 'json', method: 'POST', url: '/api/session/end', success: Ember.run.bind(null, resolve), error: Ember.run.bind(null, reject) })); } });
// app/torii-adapters/facebook-oauth2.js import ApplicationAdapter from '../torii-adapters/application'; export default ApplicationAdapter.extend({ init() { this._super('facebook'); }, open({authorizationCode}) { return this._super({ provider: this.providerName, credentials: { token: authorizationCode } }); } });
Believe it or not, that’s about it! (Most of the code is wrapping simple AJAX calls. ;-)
- Our session service will be available in routes and templates as “toriiSession” and contain the
isAuthenticated
andisWorking
properties, as well as any data passed back from the API. Torii understands thecurrentUser
property out of the box, but you can use any arbitrary property names. - It will only attempt to
open()
orfetch()
a session once—because it’s an SPA!—until the next full browser refresh. - It supports multiple provider adapters based off of a common application adapter.
- We can have multiple authenticated routes, and customize the transitions on
login()
,logout()
, andaccessDenied()
.
I’ve made the above demonstration available as a Git repository (using Ember CLI Mirage to fake the API). You’ll need to put your Facebook Application ID in config/environment.js
before starting it up. Please let me know if you have any questions or feedback.
My thanks to @mixonic and the Vestorly crew and contributors on GitHub and of course the good folks on the EmberJS Slack Community for their help figuring all this out. And a special thanks to @bmeyers22 on GitHub for solving the sessionServiceName limitation in the original writeup.
- There is a bug in Torii that may prevent the Ember initializers from loading. You might need to follow the instructions outlined here and here.
- If you have trouble validating OAuth2 codes on the server side, it may be because the client- and server-sides are using two different redirect URLs. You might have to hardcode them on each side (e.g. to
http://localhost:4200/
in development).
Pingback: Torii for Ember.js Lightning Talk | perlkour
hi there, i think this is a very good tutorial for third party apps using torii but 1 small question. isnt the session.isAuthenticated method only applicable if you have ember-simple-auth set up as well.. torii only uses session.isWorking, or am i wrong?
Hi Markie! session.isWorking tells you when Torii is in the process of authenticating. You can use it to show a spinner or prevent a user from clicking the “login” button twice. session.isAuthenticated tells you when Torii has successfully authenticated the user. Hope that helps!
Thanks for the quick response, mike! Yeah, i got sessson.isWorking to work but its only while authentication is being processed. How about once authentication succeeds and the client gets the session, any method/ helper that we could use? This tutorial uses session.isAuthenticated which is something i need but i believe it is only supported by ember-simple-auth, does it mean this app uses both torii and ember-simple-auth?
Where are you getting this idea that session.isAuthenticated is only supported by ember-simple-auth? That is not the case! Torii itself provides session.isAuthenticated. Have you tried it?
My apologies, i am an ember rookie. I just assume it was an ESA method, had no actual basis for it except that torii’s documentation never mentioned session.isAuthenticated. And my login template has both session.isAuth and session.isWorking but only “isWorking” works, so im assuming now the provider isnt sending me a session that my app can authenticate. Do u by any chance allow pm with ur readers?
No need to apologize! Sure, you can find me on the EmberJS Community Slack (@mwpastore). Slack yourself in here: https://ember-community-slackin.herokuapp.com/. See you there!