A set of clean abstractions for authentication in Ember.js
npm install toriiWe've moved torii to adopted-ember-addons to reactivate the project and make it compatible with current versions of Ember
We will shortly release a 1.0.0 that will be compatible with Ember 3.28 and upwards but will not care for older versions.
For safety reasons we have backed up the state before starting this work to the 0.10.x branch, so technically, if necessary,
we would be able to maintain a 2.x compatible version but this is currently not a goal of this particular fork.
| Torii | Ember | Ember-Data |
|-----------|---------|--------------------|
| v0.3.X and before | <= 1.13 | <= 1.0.0.beta19.2 |
| v0.4.X and after | >= 1.12 | >= 1.0.0.beta19.2 |
| v1.0.0 and after | >= 3.28 (planned) | >= 3.28 |
tl;dr; Use torii 0.3.X if your application is using Ember 1.11 or older, Use the latest 1.0.x version when on Ember 3.28 or later.
v1.0.0 removes implicit injections as these are deprecated in Ember 3.x and removed in Ember 4.x. You must explicitly inject your
session and torii services like so:
``JavaScript
import Route from '@ember/routing/route';
import AuthenticatedRouteMixin from 'torii/routing/authenticated-route-mixin';
import { inject as service } from '@ember/service';
export default class MyAuthenticatedRoute extends Route.extend(AuthenticatedRouteMixin) {
@service session;
}
`
Torii is a set of clean abstractions for authentication in Ember.js
applications. Torii is built with providers (authentication against a platform), a
session manager (for maintaining the current user), and adapters (to persist
authentication state).
The API for providers and adapters in Torii is to open, by which we mean creating a new
authorization or authenticating a new session, fetch, by which we mean validating
an existing authorization (like a session stored in cookies), or close, where an
authorization is destroyed.
A provider in Torii is anything a user can authenticate against. This could be an
OAuth 2.0 endpoint, your own login mechanism, or an SDK like Facebook Connect.
Authenticating against a provider is done via the torii property, which must be
explicitly injected into routes:
`hbs`
{{! app/templates/post.hbs }}
{{#if this.hasFacebook}}
{{partial "comment-form"}}
{{else}}
Sign in to comment
{{/if}}
`JavaScript
// app/routes/post.js
export default class PostRoute extends Route {
@service torii;
@action
signInToComment() {
var controller = this.controllerFor('post');
// The provider name is passed to open`
this.torii.open('facebook-connect').then(function(authorization){
// FB.api is now available. authorization contains the UID and
// accessToken.
controller.set('hasFacebook', true);
});
}
}
``
torii.open('facebook') -> #open hook on the facebook provider -> returned authorization
Torii can perform session management via the session service. This service must be injected explicitly intosessionServiceName
routes and controllers. You can activate session management by specifying in your config/environment.js and providing an adapter which Torii will use to extract session information from a new opened authorization.
This example uses Facebook's OAuth 2.0 API directly to fetch an authorization code.
`JavaScript`
/ jshint node: true /
// config/environment.js
module.exports = function(environment) {
var ENV = {
/ ... /
torii: {
// The name of your session service. This must be explicitly injected in routes and controllers
sessionServiceName: 'session',
providers: {
'facebook-oauth2': {
apiKey: 'facebook-app-id',
redirectUri: '/my-custom-landing-uri' // default is /torii/redirect.html
}
}
}
};
return ENV;
};
`hbs`
{{! app/templates/login.hbs }}
{{#if this.session.isWorking}}
One sec while we get you signed in...
{{else}}
{{error}}
Sign In with Facebook
{{/if}}
`JavaScript
// app/routes/login.js
export default class LoginRoute extends Route {
@service session;
@action
signInViaFacebook() {
var route = this,
controller = this.controllerFor('login');
// The provider name is passed to open`
this.session.open('facebook-oauth2').then(function(){
route.transitionTo('dashboard');
}, function(error){
controller.set('error', 'Could not sign you in: '+error.message);
});
}
}
`JavaScript
// app/torii-adapters/application.js
import EmberObject from '@ember/object';
import { Promise } from 'rsvp';
import $ from 'jquery';
import { run } from 'ember/runloop';
export default class ApplicationAdapter extends EmberObject {
open(authentication) {
var authorizationCode = authentication.authorizationCode;
return new Promise(function(resolve, reject){
$.ajax({
url: 'api/session',
data: { 'facebook-auth-code': authorizationCode },
dataType: 'json',
success: run.bind(null, resolve),
error: run.bind(null, reject)
});
}).then(function(user){
// The returned object is merged onto the session (basically). Here
// you may also want to persist the new session with cookies or via
// localStorage.
return {
currentUser: user
};
});
}
}
`
``
session.open('facebook') -> #open hook on the facebook provider -> #open hook on the application adapter -> updated session
Note that the adapter section is left entirely to your application.
Torii includes a mechanism for specifying authenticated routes via the Router.map and a common implementation of authentication flow for your application.
The authentication flow is as follows:
1. In Application Route, check if the user is logged in by calling ApplicationRoute#checkLogin which calls this.session.fetch().this.session.fetch()
2. When entering an authenticated route,
Attempt to authenticate by calling
If successful,
allow the transition to finish
otherwise,
interrupt the transition and send "accessDenied" action
This example uses Facebook's OAuth 2.0 API and the authenticatedRoute DSL.
`JavaScript`
/ jshint node: true /
// config/environment.js
module.exports = function(environment) {
var ENV = {
/ ... /
torii: {
// The name of your session service. This must be explicitly injected in routes and controllers
sessionServiceName: 'session',
providers: {
'facebook-oauth2': {
apiKey: 'facebook-app-id',
redirectUri: '/my-custom-landing-uri' // default is the current URL
}
}
}
};
return ENV;
};
`JavaScript`
// app/router.js
Router.map(function(){
this.authenticatedRoute('my-account');
this.route('login');
});
`JavaScript`
// app/routes/application.js
export default class ApplicationRoute extends Route {
@action
accessDenied() {
this.transitionTo('login');
}
}
`JavaScript
// app/torii-adapters/application.js
import EmberObject from '@ember/object';
import { Promise } from 'rsvp';
import $ from 'jquery';
import { run } from 'ember/runloop';
export default class ApplicationAdapter extends EmberObject {
open(authentication) {
var authorizationCode = authentication.authorizationCode;
return new Promise(function(resolve, reject){
$.ajax({
url: 'api/session',
data: { 'facebook-auth-code': authorizationCode },
dataType: 'json',
success: run.bind(null, resolve),
error: run.bind(null, reject)
});
}).then(function(user){
// The returned object is merged onto the session (basically). Here
// you may also want to persist the new session with cookies or via
// localStorage.
return {
currentUser: user
};
});
}
}
`
The session will automatically be populated if the user is logged in, otherwise the user will be redirected to the login page.
Using Torii currently requires an AMD-compatible module loader. Ember-CLI provide this out of the box.
Torii is an ember addon and can be installed via:
``
ember install torii
As of v0.8.0, Torii is no longer published to bower. For legacy uses, there is
an AMD build of Torii published to bower at version 0.6.1. For modern usage of
Torii, install it as an ember addon.
Now that you have added Torii to your application, you will want to
configure at least one authentication provider. Torii looks for a global
object at window.ENV.torii.providers that defines a hash of provider
names and their options.
Configure a Torii provider. Torii comes with a facebook-connecttorii
provider included. To configure torii for the 'facebook-connect'
provider with ember-cli, simply add to your config/environment.js file:
`JavaScript`
/ jshint node: true /
module.exports = function(environment) {
var ENV = {
/ ... /
torii: {
providers: {
'facebook-connect': {
appId: 'xxxxx-some-app-id',
scope: 'email,user_birthday'
}
}
}
};
return ENV;
};
With those values, we can authenticate the user against Facebook Connect
via the torii property which must be explicitly injected onto _routes_,session
or the property which similarly must be explcitly injected onto
routes and controllers (using the session management feature will require you
to write an adapter for your application – see notes on session management below).
You can configure torii to use an in-page iframe instead of a separate
popup window for authentication. This can be done on either a global or
a per-provider basis.
To change this globally set the remoteServiceName variable in the main'iframe'
torii config to be .
`JavaScript`
/ jshint node: true /
// config/environment.js
module.exports = function(environment) {
var ENV = {
/ ... /
torii: {
remoteServiceName: 'iframe',
providers: { / ... / }
}
};
return ENV;
};
If you only want the iframe for a single provider you can include the
remoteServiceName value in the configuration for that provider.
`JavaScript`
/ jshint node: true /
// config/environment.js
module.exports = function(environment) {
var ENV = {
/ ... /
torii: {
// The name of your session service. This must be explicitly injected in routes and controllers
sessionServiceName: 'session',
providers: {
'mycorp-oauth2': {
remoteServiceName: 'iframe'
/ ... /
}
}
}
};
return ENV;
};
Once your provider has been configured you need to tell torii where to
append the iframe when you call session.open by using the{{torii-iframe-placeholder}} component. You need to make sure thatsession.open
this component is added to the DOM before you call and if
you give the user a way to back out of authentication (by closing a
modal that contains the iframe, for instance) you need to make sure that
the component is removed from the DOM so that torii will see that the
auth flow has been cancelled.
For instance, in routes/application.js you might have the followingsignIn action:
`JavaScript`
@action
signIn() {
var route = this;
// Set a value that will result in the placeholder component being
// added to the DOM
route.controller.set('signingIn',true);
// We need to use run.schedule to make sure that the placeholder
// component has been added to the DOM before session.open is called
run.schedule('afterRender', this, function(){
$('#signin-modal-back').one('click',function(){
route.controller.set('signingIn',false);
});
this.session
.open("clickfunnels-oauth2")
.then(function(){
route.controller.set('signingIn',false);
});
});
}
Then in templates/application.hbs you might have:
`handlebars`
{{#if this.signingIn}}
{{torii-iframe-placeholder}}
{{/if}}
Torii was originally configured to add an initializer that detects when your
Ember app has been redirected-to by an OAuth provider, but this has been shown
to be a potential vulnerability, and best practice is to use the static
/torii/redirect.html page that the Torii addon makes available as of version
0.9.0.
Therefore, **the redirect URL you register with the OAuth provider(s) that you
use should be: **. This is a staticprovider#open
HTML page that loads no external assets and is configured to interact correctly
with Torii's promise in your app.
Torii versions after v0.8.4 will log an error message if you do not use the
Torii-provided redirect HTML page. Using your app as the redirect target is
deprecated and the functionality will be removed in later versions of Torii.
If you understand the security risks and need to continue using your app as the
redirect target, you can disable the error message by setting
allowUnsafeRedirects: true in the torii section of yourconfig/environment.js. For more details see this blog
post.
By default Torii sets the redirectUri to. If you wish to use the deprecated behaviorredirectUri
then you will also have to manually configure the to be /.
If you are no longer relying on the deprecated behavior and wish for it to no
longer be executed you can manually disable it by setting
disableRedirectInitializer to true in your config/environment.js.
Torii is built with several providers for common cases. If you intend to
use another provider for authentication, you will need to create your
own.
Providers have a single hook, open, that must be implemented. It must return a
promise:
* open creates a new authorization. An example of this is logging in a
user in with their username and password, or interfacing with an
external OAuth provider like Facebook to retrieve authorization data.
Torii will lookup providers in the Ember application container, so if you
name them conventionally (put them in the app/torii-providers directory)
they will be available automatically when using ember-cli or ember app
kit.
A minimal provider:
`JavaScript
// app/torii-providers/geocities.js
import EmberObject from '@ember/object';
import { Promise } from 'rsvp';
export default class GeocitiesProvider extends EmberObject {
// Create a new authorization.
// When your code calls this.torii.open('geocities', options),options
// the will be passed to this provider's open method.
open(options) {
return new Promise(function(resolve, reject){
// resolve with an authorization object
});
}
}
`
Provider hooks should return a promise resolving with an authorization
object. Authorization objects should include values like access tokens, or
an Ember-Data model representing a session, or minimal user data like UIDs.
They may return SDK objects, such as an object with an API for making
authenticated calls to an API.
When used via torii.open, the authorization object is passed through to
the consumer. An example provider called 'geocities':
`JavaScript
// app/torii-providers/geocities.js
import EmberObject from '@ember/object';
import { Promise } from 'rsvp';
import { bind } from '@ember/runloop';
export default class GeocitiesProvider extends EmberObject {
// credentials as passed from torii.open
open(credentials){
return new Promise(function(resolve, reject){
exampleAsyncLogin(
credentials.username,
credentials.password,
// callback function:
function(error, response) {
// the promise is resolved with the authorization
bind(null, resolve, {sessionToken: response.token});
}
);
});
}
}
`
`JavaScript
// app/routes/application.js
export default class ApplicationRoute extends Route {
@service torii;
@action
openGeocities(username, password) {
var route = this;
var providerName = 'geocities';
// The options to this.torii.open(providerName, options) willopen
// be passed to the provider's method.
var options = {
username: username,
password: password
};
this.torii.open(providerName, options).then(function(authorization) {
// authorization as returned by the provider
route.somethingWithGeocitiesToken(authorization.sessionToken);
});
}
}
`
The cornerstone of many Torii providers is the popup object, which is injected
onto all providers.
Torii comes with several providers already included:
* Github OAuth2 (Dev Site | Docs)
* LinkedIn OAuth2 (Dev Site | Docs)
* Google OAuth2 (Dev Site | Docs)
* Facebook Connect (via FB SDK) (Dev Site | Docs)
* Facebook OAuth2 (Dev Site | Docs)
* Stripe Connect (OAuth2) (Dev Site | Docs)
* Authoring custom providers is designed to be easy - You are encouraged to author your own.
OAuth 1.0a, used by Twitter and some other organizations, requires a significant
server-side component and so cannot be supported out of the box. It can be implemented
following these steps:
1. Torii provider opens a popup to the app server asking for Twitter auth
2. Server redirects to Twitter with the credentials for login
3. User enters their credentials at Twitter
4. Twitter redirects to app server which completes the authentication
5. Server loads the Ember application with a message in the URL, or otherwise
transmits a message back to the parent window.
6. Ember application in the initial window closes the popup and resolves its
provider promise.
If you want to use torii's session management state machine, you _must_ opt in to it via the torii configuration.
Because of the potential for conflicts, torii will not inject a session property unless you explicitly ask for
it in your configuration. To do so, specify a sessionServiceName in your torii config.
To add a session service in Ember-CLI, simply:
`JavaScript`
// config/environment.js
/ ... /
torii: {
// The name of your session service. This must be explicitly injected in routes and controllers
sessionServiceName: 'session'
}
/ ... /
Or to do the same in a global configuration
`JavaScript
window.ENV = window.ENV || {};
window.ENV['torii'] = {
sessionServiceName: 'session', // The name of your session service. This must be explicitly injected in routes and controllers
// ... additional configuration for providers, etc
};
`
Read on about adapters for more information on using torii's session management.
Adapters in Torii process authorizations and pass data to the session. For
example, a provider might create an authorization object from the Facebook
OAuth 2.0 API, then create a session on your applications server. The adapter
would then fetch the user and resolve with that value, adding it to the
sessions object.
Again, adapters are looked up on the container, and so if you name them
conventionally (put the in app/torii-adapters/) then they are loaded
automatically.
Adapters have three hooks that may be implemented. Each must return a
promise:
* open - a new sessionfetch
* - a refreshed sessionclose
* - a closing session
Adapters are flexible, but a common use would be to fetch a current user
for the session. By default, the application adapter will handle allopen
authorizations. An example application adapter with an hook:
`JavaScript
// app/torii-adapters/application.js
//
export default class ApplicationAdapter extends EmberObject {
@service store; // inject the ember-data store
// The authorization argument passed in to session.open here istorii.open(providerName)
// the result of the promise
open(authorization){
var userId = authorization.user;
return this.store.find('user', userId).then(function(user) {
return {
currentUser: user
};
});
}
}
`
The object containing the currentUser is merged onto the session. Because the
session is injected onto controllers and routes, these values will be available
to templates.
Torii will first look for an adapter matching the provider name passed to
session.open (i.e., if you do session.open('geocities'), torii first lookstorii-adapters/geocities
for an adapter at ). If there is no matching adapter,application
then the session object will fall back to using the adapter.
For testing code that interacts with torii it can be useful to stub a
valid session. Torii inculdes a test helper for stubbing sessions during
acceptance testing.
Import the stubValidSession helper method.
`javascript`
import { stubValidSession } from 'your-app-name/tests/helpers/torii';
Pass the test application, and a second argument that is treated like theopen
return value from an adapter or fetch hook. The properties become
accessible on the session itself.
`javascript`
stubValidSession(application, {
currentUser: {
handle: 'testguy',
uid: 'xyz'
}
});
A more complete example follows:
`javascript
import { stubValidSession } from 'your-app/tests/helpers/torii';
/ test boilerplate /
test('shows something when signed in', function(assert) {
stubValidSession(application, {currentUser});
visit('/');
andThen(function() {
// make assertions here assuming that the session is set with the object passed above...
});
});
`Third-Party Addons
There are a number of ember-cli addons that allow you to use Torii with other providers:
* torii-provider-meetup-oauth2 via @gmurphey
* torii-spotify via @balinterdi
* torii-azure-provider via @ghurlman
* torii-azure-ad2-provider via @erikap
* torii-provider-arcgis via @dbouwman
* torii-globe via @jedld
* torii-edmodo-connect via @YoranBrondsema
* Clone the repo git clone git@github.com:adopted-ember-addons/torii.git, cd torii/pnpm install
* pnpm ember test
* for tests.pnpm ember test --server
* Or, to run tests in the browser:
* Start the server:
The torii example OAuth apps (at Facebook, Google, LinkedIn, etc.) are all
configured to use
http://torii-example.com:4200/torii/redirect.html as their redirect
uri, so you will need to make an alias in your hosts file that points
torii-example.com to localhost, and you must view the examples from
that same host.
To add this hostname on a Mac:
* sudo vim /etc/hosts127.0.0.1 torii-example.com
* Add line like this:
The /etc/hosts equivalent filepath on Windows is:%SystemRoot%\system32\drivers\etc\hosts.
For more info, see Hosts at wikipedia).
* Clone the repo git clone git@github.com:Vestorly/torii.git, cd torii/pnpm install
* ember serve
*
Now, start your server and visit the page:
* ember servehttp://torii-example.com:4200
* open
If you discover a vulnerability in Torii please inform us by emailing
security@201-created.com. You can encrypt the message using our public
key.
* Bump version in package.json; commit
* git tag git push --tags
* npm publish ./`
*
*Initial development of Torii was generously funded by
Vestorly. Vestorly is a technology company
solving the client acquisition problem for professionals in wealth
management, and the enterprises that support them. Vestorly's user
interface is built entirely with Ember.js and modern web technologies.
hello@vestorly.com*
Torii aims to provide a flexible set of primitives for creating your
application' own authentication solution. There are always a few things
we could use help with.
We welcome your contributions!
As a member of the adopted ember addons family, this project uses the Ember community guidelines