Skip to content

Latest commit

 

History

History
872 lines (670 loc) · 30.3 KB

README.md

File metadata and controls

872 lines (670 loc) · 30.3 KB

Build Status

Ember Simple Auth API docs

Discord

Ember Simple Auth supports all Ember.js versions starting with 1.12.

Ember Simple Auth

Logo

Ember Simple Auth is a lightweight library for implementing authentication/ authorization with Ember.js applications. It has minimal requirements with respect to application structure, routes etc. With its pluggable strategies it can support all kinds of authentication and authorization mechanisms.

Table of Contents

Basic Information

Usage

Core Feature Guides

Other Guides

Other Resources

What does it do?

  • it maintains a client side session and synchronizes its state across multiple tabs/windows of the application
  • it authenticates the session against the application's own server, external providers like Facebook etc.
  • it authorizes requests to backend servers
  • it is easily customizable and extensible

How does it work?

Ember Simple Auth consists of 4 main building blocks - the session, a session store, authenticators and (optionally) authorizers.

The session service is the main interface to the library. It provides methods for authenticating and invalidating the session as well as for setting and reading session data.

The session store persists the session state so that it survives a page reload. It also synchronizes the session state across multiple tabs or windows of the application so that e.g. a logout in one tab or window also results in a logout in all other tabs or windows of the application.

Authenticators authenticate the session. An application can leverage multiple authenticators to support multiple ways of authentication such as sending credentials to the application's own backend server, Facebook, github etc.

Authorizers use the data retrieved by an authenticator and stored in the session to generate authorization data that can be injected into outgoing requests such as Ember Data requests.

Example App

Ember Simple Auth comes with a dummy app that implements a complete auth solution including authentication against the application's own server as well as Facebook, authorization of Ember Data requests and error handling. Check out that dummy app for reference. To start it, run

git clone https://github.com/simplabs/ember-simple-auth.git
cd ember-simple-auth
yarn install && ember serve

and go to http://localhost:4200.

Installation

Installing the library is as easy as:

ember install ember-simple-auth

Upgrading from ember-cli-simple-auth / pre-1.0 release?

The 1.0 release of ember-simple-auth introduced a lot of breaking changes, but thankfully the upgrade path isn't too hard.

Walkthrough

Once the library is installed, the session service can be injected wherever needed in the application. In order to display login/logout buttons depending on the current session state, inject the service into the respective controller or component and query its isAuthenticated property in the template:

// app/controllers/application.js
import Controller from '@ember/controller';
import { inject as service } from '@ember/service';

export default Controller.extend({
  session: service()

  
});
{{!-- app/templates/application.hbs --}}
<div class="menu">
  …
  {{#if session.isAuthenticated}}
    <a {{action 'invalidateSession'}}>Logout</a>
  {{else}}
    {{#link-to 'login'}}Login{{/link-to}}
  {{/if}}
</div>
<div class="main">
  {{outlet}}
</div>

In the invalidateSession action call the session service's invalidate method to invalidate the session and log the user out:

// app/controllers/application.js
import Controller from '@ember/controller';
import { inject as service } from '@ember/service';

export default Controller.extend({
  session: service(),

  

  actions: {
    invalidateSession() {
      this.get('session').invalidate();
    }
  }
});

For authenticating the session, the session service provides the authenticate method that takes the name of the authenticator to use as well as other arguments depending on specific authenticator used. To define an authenticator, add a new file in app/authenticators and extend one of the authenticators the library comes with, e.g.:

// app/authenticators/oauth2.js
import OAuth2PasswordGrant from 'ember-simple-auth/authenticators/oauth2-password-grant';

export default OAuth2PasswordGrant.extend();

With that authenticator and a login form like

{{!-- app/templates/login.hbs --}}
<form {{action 'authenticate' on='submit'}}>
  <label for="identification">Login</label>
  {{input id='identification' placeholder='Enter Login' value=identification}}
  <label for="password">Password</label>
  {{input id='password' placeholder='Enter Password' type='password' value=password}}
  <button type="submit">Login</button>
  {{#if errorMessage}}
    <p>{{errorMessage}}</p>
  {{/if}}
</form>

the session can be authenticated with the session service's authenticate method:

// app/controllers/login.js
import Controller from '@ember/controller';
import { inject as service } from '@ember/service';

export default Controller.extend({
  session: service(),

  actions: {
    authenticate() {
      let { identification, password } = this.getProperties('identification', 'password');
      this.get('session').authenticate('authenticator:oauth2', identification, password).catch((reason) => {
        this.set('errorMessage', reason.error || reason);
      });
    }
  }
});

The session service also provides the authenticationSucceeded and invalidationSucceeded events that are triggered whenever the session is successfully authenticated or invalidated (which not only happens when the user submits the login form or clicks the logout button but also when the session is authenticated or invalidated in another tab or window of the application). To have these events handled automatically, simply mix ApplicationRouteMixin into the application route:

// app/routes/application.js
import Route from '@ember/routing/route';
import ApplicationRouteMixin from 'ember-simple-auth/mixins/application-route-mixin';

export default Route.extend(ApplicationRouteMixin);

The ApplicationRouteMixin automatically maps the session events to the sessionAuthenticated and sessionInvalidated methods it implements. The sessionAuthenticated method will transition to a configurable route while the sessionInvalidated method will reload the page to clear all potentially sensitive data from memory.

To make a route in the application accessible only when the session is authenticated, mix the AuthenticatedRouteMixin into the respective route:

// app/routes/protected.js
import Route from '@ember/routing/route';
import AuthenticatedRouteMixin from 'ember-simple-auth/mixins/authenticated-route-mixin';

export default Route.extend(AuthenticatedRouteMixin);

This will make the route (and all of its subroutes) transition to the login route if the session is not authenticated. Add the login route in the router like this:

// app/router.js
Router.map(function() {
  this.route('login');
});

The route to transition to if the session is not authenticated can also be overridden to be another one than login.

It is recommended to nest all of an application's routes that require the session to be authenticated under a common parent route:

// app/router.js
Router.map(function() {
  this.route('login');
  this.route('authenticated', { path: '' }, function() {
    // all routes that require the session to be authenticated
  });
});

To prevent a route from being accessed when the session is authenticated (which makes sense for login and registration routes for example), mix the UnauthenticatedRouteMixin into the respective route.

In order to add authorization information to outgoing API requests the application can define an authorizer. To do so, add a new file to app/authorizers, e.g.:

// app/authorizers/oauth2.js
import OAuth2Bearer from 'ember-simple-auth/authorizers/oauth2-bearer';

export default OAuth2Bearer.extend();

and use that to authorize a block of code via the session service's authorize method, e.g.:

this.get('session').authorize('authorizer:oauth2', (headerName, headerValue) => {
  const headers = {};
  headers[headerName] = headerValue;
  Ember.$.ajax('/secret-data', { headers });
});

To include authorization info in all Ember Data requests if the session is authenticated, mix the DataAdapterMixin into the application adapter:

// app/adapters/application.js
import DS from 'ember-data';
import DataAdapterMixin from 'ember-simple-auth/mixins/data-adapter-mixin';

const { JSONAPIAdapter } = DS;

export default JSONAPIAdapter.extend(DataAdapterMixin, {
  authorizer: 'authorizer:oauth2'
});

The Session Service

The session service is the main interface to the library. It defines the authenticate, invalidate and authorize methods as well as the session events as shown above.

It also provides the isAuthenticated as well as the data properties. The latter can be used to get and set the session data. While the special authenticated section in the session data contains the data that was acquired by the authenticator when it authenticated the session and is read-only, all other session data can be written and will also remain in the session after it is invalidated. It can be used to store all kinds of client side data that needs to be persisted and synchronized across tabs and windows, e.g.:

this.get('session').set('data.locale', 'de');

Authenticators

Authenticators implement the concrete steps necessary to authenticate the session. An application can leverage several authenticators for different kinds of authentication mechanisms (e.g. the application's own backend server, external authentication providers like Facebook etc.) while the session is only ever authenticated with one authenticator at a time. The authenticator to use is chosen when authentication is triggered via the name it is registered with in the Ember container:

this.get('session').authenticate('authenticator:some');

Ember Simple Auth comes with 4 authenticators:

To use any of these authenticators in an application, define a new authenticator in app/authenticators, extend if from the Ember Simple Auth authenticator

// app/authenticators/oauth2.js
import OAuth2PasswordGrantAuthenticator from 'ember-simple-auth/authenticators/oauth2-password-grant';

export default OAuth2PasswordGrantAuthenticator.extend();

and invoke the session service's authenticate method with the respective name, specifying more arguments as needed by the authenticator:

this.get('session').authenticate('authenticator:some', data);

Customizing an Authenticator

Authenticators are easily customized by setting the respective properties, e.g.:

// app/authenticators/oauth2.js
import OAuth2PasswordGrantAuthenticator from 'ember-simple-auth/authenticators/oauth2-password-grant';

export default OAuth2PasswordGrantAuthenticator.extend({
  serverTokenEndpoint: '/custom/endpoint'
});

Implementing a custom Authenticator

Besides extending one of the predefined authenticators, an application can also implement fully custom authenticators. In order to do that, extend the abstract base authenticator that Ember Simple Auth comes with and override the authenticate, restore and (optionally) invalidate methods:

// app/authenticators/custom.js
import Base from 'ember-simple-auth/authenticators/base';

export default Base.extend({
  restore(data) {
    
  },
  authenticate(options) {
    
  },
  invalidate(data) {
    
  }
});

Authorizers

Authorizers use the session data acquired by the authenticator to construct authorization data that can be injected into outgoing network requests. As Deprecation warning: Authorizers are deprecated

the authorizer depends on the data that the authenticator acquires, authorizers and authenticators have to fit together.

Ember Simple Auth comes with 2 authorizers:

To use any of these authorizers in an application, define a new authorizer in app/authorizers, extend if from the Ember Simple Auth authorizer

// app/authorizers/oauth2.js
import OAuth2Bearer from 'ember-simple-auth/authorizers/oauth2-bearer';

export default OAuth2Bearer.extend();

and invoke the session service's authorize method with the respective name:

this.get('session').authorize('authorizer:some', (/*authorization data*/) => {
  // Use authorization data
});

Unlike in previous versions of Ember Simple Auth, authorization will not happen automatically for all requests the application issues anymore but has to be initiated explicitly via the service.

When using Ember Data you can mix the DataAdapterMixin in the application adapter to automatically authorize all API requests:

// app/adapters/application.js
import DS from 'ember-data';
import DataAdapterMixin from 'ember-simple-auth/mixins/data-adapter-mixin';

const { JSONAPIAdapter } = DS;

export default JSONAPIAdapter.extend(DataAdapterMixin, {
  authorizer: 'authorizer:some'
});

Customizing an Authorizer

Authorizers are easily customized by setting the respective properties, e.g.:

// app/authorizers/devise.js
import DeviseAuthorizer from 'ember-simple-auth/authorizers/devise';

export default DeviseAuthorizer.extend({
  identificationAttributeName: 'login'
});

Implementing a custom Authorizer

Besides extending one of the predefined authorizers, an application can also implement fully custom authorizers. In order to do that, extend the abstract base authorizer that Ember Simple Auth comes with and override the authorize method:

// app/authorizers/custom.js
import Base from 'ember-simple-auth/authorizers/base';

export default Base.extend({
  authorize(sessionData, block) {
    
  }
});

Deprecation of Authorizers

Authorizers and the session service's authorize method are deprecated and will be removed from Ember Simple Auth 2.0. The concept seemed like a good idea in the early days of Ember Simple Auth, but proved to provide limited value for the added complexity. To replace authorizers in an application, simply get the session data from the session service and inject it where needed.

In most cases, authorizers are used with Ember Data adapters (refer to the Ember Guides for details on adapters). Replacing authorizers in these scenarios is straightforward.

Examples:

// OAuth 2
import DS from 'ember-data';
import { inject as service } from '@ember/service';
import { isPresent } from '@ember/utils';
import DataAdapterMixin from "ember-simple-auth/mixins/data-adapter-mixin";

const { JSONAPIAdapter } = DS;

export default JSONAPIAdapter.extend(DataAdapterMixin, {
  session: service(), 
  authorize(xhr) {
    let { access_token } = this.get('session.data.authenticated');
    if (isPresent(access_token)) {
      xhr.setRequestHeader('Authorization', `Bearer ${access_token}`);
    }
  }
});

// DataAdapterMixin already injects the `session` service. It is
// included here for clarity.
// Devise
import DS from 'ember-data';
import { inject as service } from '@ember/service';
import DataAdapterMixin from "ember-simple-auth/mixins/data-adapter-mixin";

const { JSONAPIAdapter } = DS;

export default JSONAPIAdapter.extend(DataAdapterMixin, {
  session: service(),
  // defaults
  // identificationAttributeName: 'email'
  // tokenAttributeName: 'token'
  authorize(xhr) {
    let { email, token } = this.get('session.data.authenticated');
    let authData = `Token token="${token}", email="${email}"`;
    xhr.setRequestHeader('Authorization', authData);
  }
});

When used with ember-fetch the authorize method will not be called and the headers computed property must be used instead, e.g.:

export default DS.JSONAPIAdapter.extend(AdapterFetch, DataAdapterMixin, {
  headers: computed('session.data.authenticated.token', function() {
    const headers = {};
    if (this.session.isAuthenticated) {
      headers['Authorization'] = `Bearer ${this.session.data.authenticated.token}`;
    }

    return headers;
  }),
});

Deprecation of Client ID as Header

Sending the Client ID as Base64 Encoded in the Authorization Header was against the spec and caused incorrect behavior with OAuth2 Servers that had implemented the spec properly.

To change this behavior set sendClientIdAsQueryParam to true, and the client id will be correctly sent as a query parameter. Leaving it set to false (currently default) will result in a deprecation notice until the next major version.

Session Stores

Ember Simple Auth persists the session state via a session store so it survives page reloads. There is only one store per application that can be defined in app/session-stores/application.js:

// app/session-stores/application.js
import Cookie from 'ember-simple-auth/session-stores/cookie';

export default Cookie.extend();

If the application does not define a session store, the adaptive store which uses localStorage if that is available or a cookie if it is not, will be used by default. To customize the adaptive store, define a custom store in app/session-stores/application.js that extends it and overrides the properties to customize.

Store Types

Ember Simple Auth comes with 4 stores:

Adaptive Store

The adaptive store stores its data in the browser's localStorage if that is available or in a cookie if it is not; this is the default store.

localStorage Store

The localStorage store stores its data in the browser's localStorage. This is used by the adaptive store if localStorage is available.

Cookie Store

The Cookie store stores its data in a cookie. This is used by the adaptive store if localStorage is not available. This store must be used when the application uses FastBoot.

sessionStorage Store

The sessionStorage store stores its data in the browser's sessionStorage. See the Web Storage docs for details on sessionStorage and localStorage. caniuse has up-to-date information on browser support of sessionStorage and localStorage.

Ephemeral Store

The ephemeral store stores its data in memory and thus is not actually persistent. This store is mainly useful for testing. Also the ephemeral store cannot keep multiple tabs or windows in sync as tabs/windows cannot share memory.

Customizing the Store

The session store is easily customized by setting the respective properties, e.g.:

// app/session-stores/application.js
import AdaptiveStore from 'ember-simple-auth/session-stores/adaptive';

export default AdaptiveStore.extend({
  cookieName: 'my-apps-session-cookie'
});

Implementing a custom Store

Besides using one of the predefined session stores, an application can also implement fully custom stores. In order to do that, extend the abstract base session store that Ember Simple Auth comes with and implement the persist, restore and clear methods:

// app/session-stores/application.js
import Base from 'ember-simple-auth/session-stores/base';

export default Base.extend({
  persist() {
    
  },

  restore() {
    
  }
});

FastBoot

Ember Simple Auth works with FastBoot out of the box as long as the Cookie session store is being used. In order to enable the cookie store, define it as the application store:

// app/session-stores/application.js
import CookieStore from 'ember-simple-auth/session-stores/cookie';

export default CookieStore.extend();

If you are using the OAuth2PasswordGrantAuthenticator, or DeviseAuthenticator, you must add node-fetch to your list of FastBoot whitelisted dependencies in package.json:

{
  "fastbootDependencies": [
    "node-fetch"
  ]
}

Testing

Ember Simple Auth comes with a set of test helpers that can be used in acceptance tests.

ember-cli-qunit 4.2.0 and greater or ember-qunit 3.2.0 and greater

If your app is using ember-cli-qunit 4.2.0 or greater or ember-qunit 3.2.0 or greater, you may want to migrate to the more modern testing syntax. In that case, helpers can be imported from the ember-simple-auth addon namespace.

// tests/acceptance/…
import { currentSession, authenticateSession, invalidateSession} from 'ember-simple-auth/test-support';

The new-style helpers have the following function signatures:

  • currentSession() returns the current session.
  • authenticateSession(sessionData) authenticates the session asynchronously; the optional sessionData argument can be used to mock an authenticator response (e.g. a token or user).
  • invalidateSession() invalidates the session asynchronously.

New tests using the async authenticateSession helper will look like this:

import { module, test } from 'qunit';
import { setupApplicationTest } from 'ember-qunit';
import { currentURL, visit } from '@ember/test-helpers';
import { authenticateSession } from 'ember-simple-auth/test-support';

module('Acceptance | super secret url', function(hooks) {
  setupApplicationTest(hooks);

  test('authenticated users can visit /super-secret-url', async function(assert) {
    await authenticateSession({
      userId: 1,
      otherData: 'some-data'
    });
    await visit('/super-secret-url');
    assert.equal(currentURL(), '/super-secret-url', 'user is on super-secret-url');
  });
});

ember-cli-qunit 4.1.0 and earlier

For apps using earlier versions of ember-cli-qunit, you can use the test helpers with the following signature:

  • currentSession(this.application): returns the current session of your test application.
  • authenticateSession(this.application, sessionData): authenticates the session; the optional sessionData argument can be used to mock an authenticator response - e.g. a token.
  • invalidateSession(this.application): invalidates the current session in your test application.

For existing apps, the test helpers are merged into your application's namespace, and can be imported from the helpers/ember-simple-auth module like this:

// tests/acceptance/…
import { currentSession, authenticateSession, invalidateSession } from '<app-name>/tests/helpers/ember-simple-auth';

The test helpers used in apps using ember-cli-qunit 4.1.0 and earlier all require access to the test application instance.

An application instance is automatically created for you once you use the moduleForAcceptance test helper that is provided in the acceptance test blueprint. The app instance created through moduleForAcceptance is available as this.application in your test cases:

import moduleForAcceptance from '<your-app-name>/tests/helpers/module-for-acceptance';

// creates and destroys a test application instance before / after each test case
moduleForAcceptance('Acceptance | authentication');

test('user is authenticating', function(assert) {
  // returns the instance of your test application
  let app = this.application;
});

Pass in your application instance as a first parameter to the test helper functions to get a handle on your application's session store in your subsequent test cases. Here is a full example of how an acceptance test might look like if your test suite is leveraging ember-qunit:

import Ember from 'ember';
import { test } from 'qunit';
import moduleForAcceptance from 'simple-tests/tests/helpers/module-for-acceptance';
import { currentSession, authenticateSession } from 'simple-tests/tests/helpers/ember-simple-auth';

moduleForAcceptance('Acceptance | authentication');

test('user is authenticating', function(assert) {
  visit('/login');

  andThen(() => {
    assert.equal(currentURL(), '/login');
    assert.notOk(currentSession(this.application).get('isAuthenticated'), 'the user is yet unauthenticated');

    // this will authenticate the current session of the test application
    authenticateSession(this.application, { token: 'abcdDEF', token_type: 'Bearer' });

    andThen(() => {
      assert.ok(currentSession(this.application).get('isAuthenticated'), 'the user is authenticated');
      assert.deepEqual(currentSession(this.application).get('data.authenticated'), {
        authenticator: 'authenticator:test',
        token: 'abcdDEF',
        token_type: 'Bearer'
      });
    });
  });
});

If you're an ember-mocha user, we can recommend to check out this example from the test suite of ember-simple-auth itself.

Other guides

License

Ember Simple Auth is developed by and © simplabs GmbH and contributors. It is released under the MIT License.

Ember Simple Auth is not an official part of Ember.js and is not maintained by the Ember.js Core Team.