Add Twitter Authentication to an Ember.js App with Torii
Torii is a lightweight authentication library for Ember.js. It supports various OAuth providers (such as Twitter, Google or FaceBook), and can be used to implement a pop-up-based OAuth redirect flow. It uses a session manager (for maintaining the current user), and adapters (to persist authentication state).
In this article I will demonstrate how to implement sign in with Twitter and how to handle user sessions with Torii. Twitter uses OAuth 1.0a for authentication, which doesn’t require much client-side setup (just the pop-up and the session management). It does however require a significant server-side component for which I will use Sinatra.
For those wishing to follow along, you can grab the code to accompany this article from GitHub.
Setting up an Application on Twitter
If you do want to follow along, you will also need to setup an application on Twitter. You can do this by going to http://apps.twitter.com, where you click on “Create New App”. After that fill out your details making sure to enter http://127.0.0.1:9292
into the callback URL field (assuming you are testing locally).
Once you have created your app, you will be redirected to a page with your application’s settings. Click the “Keys and Access Tokens” tab and make a note of your consumer key and your consumer secret.
Server Setup
This requires a little knowledge of how OAuth 1.0a works (if you would like a refresher, you can check out the documentation on Twitter’s website). It also requires a library that supports authentication with different OAuth providers. As we are using Sinatra, OmniAuth is an excellent choice (it is built upon Rack, so will work in pretty much any Ruby web application). If we were using Node, we could have opted for Passport instead.
Rather than walk you through the server set up, you can just grab a working copy of the app and boot it up yourselves. Here’s how:
git clone https://github.com/sitepoint-editors/torii-twitter-example.git
cd torii-twitter-example
Then, in your terminal add your consumer key and your consumer secret to your environment
export TWITTER_KEY=twitter_key TWITTER_SECRET=twitter_secret
Run bundle
to install any dependencies (assumes you have Ruby installed), then rake db:migrate
to set up the database.
After that you need to build the Ember application:
cd public
npm install && bower install
ember build
Finally run rackup
to start Sinatra and navigate to http://127.0.0.1:9292
. If everything has gone as planned, you should be able to sign in to your new app using Twitter.
Note that the server endpoints are as follows:
Unauthenticated User:
get '/auth/twitter'
: Starts a sign in flow, requests a token from Twitter, redirects user to Twitter for authentication.get '/auth/twitter/callback'
: Twitter authenticates and sends token here, server exchanges token for an access token and authenticates user.
Authenticated User:
post '/logout'
: Clears user authentication.get '/users/me'
: Returns authenticated user info.
Now let’s use our app to look at how you might implement Torii in your own projects.
Install Torii
First, setup an Ember project with Ember CLI, and install Torii (ours is installed in the public
folder):
ember init
npm install –save-dev torii
Configure a Provider
Next, add the Twitter provider and set requestTokenUri
to the server endpoint where the flow starts: /auth/twitter
. Also set the sessionServiceName: 'session'
to setup the session manager.
config/environment.js
ENV.torii = {
sessionServiceName: 'session',
providers: {
'twitter': {
requestTokenUri: '/auth/twitter'
}
}
};
Torii has several built-in providers, yet authoring your own is designed to be easy.
Sign In
Next, setup a sign in template. If the user is authenticated, show their user name and a logout link. If they’re not authenticated, show the sign in link. It makes sense to put this in the application
template so that it’s visible in every route.
app/templates/application.hbs
{{#if session.isWorking }}
Working..
{{else}}
{{#if session.isAuthenticated }}
<p>Welcome {{session.currentUser.name}}
<a href="#" {{action 'logout'}}>Logout</a>
</p>
{{else}}
<a href="#" {{action 'signInViaTwitter'}}>Login via Twitter</a>
{{/if}}
{{/if}}
The session
property is injected by Torri’s session manager and exposes several useful properties. session.isWorking
is true between such state transitions as opening
to authenticated
, or closing
to unauthenticated
. Don’t use session
between transitions, but show a loading bar instead. session.currentUser
is the authenticated user—it is set by the adapter.
Then, define a signInViaTwitter
action which will open a pop-up and start the OAuth sign in flow.
app/routes/login.js
actions: {
signInViaTwitter: function() {
var route = this;
this.get('session').open('twitter').then(function() {
route.transitionTo('index');
}, function() {
console.log('auth failed');
});
}
}
Note that this.get('session').open('twitter')
returns a promise that is resolved after it authenticates the user, which in turn closes the pop-up and sets up the session. Once the user session is established, it transitions to the index route, whereas if it fails, it does nothing.
If the user refreshes the browser, or opens it for the first time, while the session is alive, the application should fetch the existing session and continue as if user has already signed in. Finally if the user logs out, the session should be destroyed.
app/routes/application.js
export default Ember.Route.extend({
beforeModel: function() {
return this.get('session').fetch().then(function() {
console.log('session fetched');
}, function() {
console.log('no session to fetch');
});
},
actions: {
logout: function() {
this.get('session').close();
}
}
});
Here this.get('session').fetch()
fetches the existing session, and sets the user as authenticated. We place this in the beforeModel
hook so that the application will wait until the user is fetched before rendering. As you might expect, this.get('session').close()
closes the session—which happens when user clicks “Logout”.
Session Management and Adapter
A Torii adapter processes the server authentication and manages a user session via three methods, open
, fetch
, and close
. They go in the folder app/torii-adapters
. The default is an application adapter, that extends Ember.Object
.
The open
method creates the session. It does this when our server sends an authentication token to the Ember application (via redirecting the pop-up) with a code
parameter, such as /?code=authentication_code
. This code
is parsed by Torii and passed to our adapter in the auth
parameter. To describe the flow briefly:
- Torii opens a pop-up to
/auth/twitter
. - Server redirects to Twitter.
- User authenticates with Twitter.
- Twitter redirects to
/auth/twitter/callback
. - Server authenticates the user, and generates an access token.
- Server redirects to our Ember application with the access token, e.g.:
/?code=access_token
- Torii closes the pop-up, parses the code and passes it to the adapter.
Once the token is available, it is placed in local storage and the authorization header for the Ajax requests is set. The authenticated user is retrieved by sending an Ajax request to users/me
, and stored in the session.
app/torii-adapters/application.js
open: function(auth) {
if (!auth.code) {
return rejectPromise();
}
localStorage.token = auth.code;
var adapter = this.container.lookup('adapter:application');
adapter.set('headers', { 'Authorization': localStorage.token });
return this.get('store').find('user', 'me').then(function(user) {
return {
currentUser: user
};
});
},
The auth
parameter contains the code—if it’s not available, the promise is rejected and authentication fails. The way to set the headers for Ember Data is to use this.container.lookup
to find adapter:application
and set the headers on that adapter. this.get('store').find('user', 'me')
sends a request to users/me
, before an object with a currentUser
property (set to the authenticated user) is returned. Torii will add this to the session
object which it will inject into all the of routes, so that it will be available in the templates.
Note: You’ll need to define a user model with Ember CLI to make a request against users/me
endpoint with Ember Data:
ember g model user name:string token:string
The fetch
method checks for an existing token in local storage. If it exists, it fetches the token and sets up the session. This keeps the user authenticated between page refreshes, as long as the token is valid and it stays in local storage.
fetch: function() {
if (!localStorage.token) {
return rejectPromise();
}
var adapter = this.container.lookup('adapter:application');
adapter.set('headers', { 'Authorization': localStorage.token });
return this.get('store').find('user', 'me').then(function(user) {
return {
currentUser: user
};
});
},
The method of fetching the authenticated user with a token, is to fetch the user from users/me
. Torii is agnostic about how to keep sessions, as long as you provide the logic via adapter hooks. Please feel free to share any alternative methods you might have.
Finally, the close
method closes the session. It removes the token from local storage, and makes a post /logout
Ajax request to the server, which will invalidate the access token.
close: function() {
var authToken = localStorage.token;
localStorage.token = null;
var adapter = this.container.lookup('adapter:application');
adapter.set('headers', { 'Authorization': authToken });
return new Ember.RSVP.Promise(function(resolve, reject) {
Ember.$.ajax({
url: '/logout',
headers: {
'Authorization': authToken
},
type: 'POST',
success: Ember.run.bind(null, resolve),
error: Ember.run.bind(null, reject)
});
});
}
Conclusion
It took some time for me to grasp how authentication should work for a single page application, not to mention how OAuth works. This is especially true if the application is communicating with a REST API, which is supposed to be stateless and thus no server side session is available to persist a user. So, I prefer token based authentication. Ember is unfortunately lacking in such tutorials, so if you want to learn more, you should search for other frameworks such as AngularJS.
Here is some further some reading which you might find useful: