An ember-cli addon for using Auth0 with Ember Simple Auth.
Auth0's Lock widget and Universal Login page are nice ways to get a fully functional signup and login workflow into your app. This addon makes it dead simple to add one or the other to your Ember application.
Basic Information
Installation
Usage
Feature Guides
Migration Guides
- Migrating from Ember-Simple-Auth-Auth0 v4.x to v5.x
- Migrating from Ember-Simple-Auth-Auth0 v3.x to v4.x
Developing this Addon
License
- it wires up Auth0's Lock.js and its hosted Universal Login to work with Ember Simple Auth.
- it lets you work with Ember Simple Auth just like you normally do!
This addon ships with a dead simple dummy app that can be used as a template for starting new projects. Alternatively, this readme details how to get it up and running from scratch, and details some more advanced features and use cases.
If you don't already have an account, go sign up at Auth0 for free, then:
- Create a new app through your dashboard.
- Add
http://localhost:4200
to your Allowed Callback URLs through your dashboard - If you wish to use a hosted login page (i.e. Universal Login), enable it through your dashboard
- That's it!
To use this addon, simply install it with ember-cli:
ember install ember-simple-auth-auth0
All dependencies such as Auth0.js, Lock, and ember-simple-auth will be pulled in automatically, so that's it!
In your config/environment.js
file, provide the following properties:
- (REQUIRED) - clientID - Get this from your Auth0 Dashboard
- (REQUIRED) - domain - Get this from your Auth0 Dashboard
- (OPTIONAL) - logoutReturnToURL - This can be overridden if you have a different logout callback than the login page.
- (OPTIONAL) - enableImpersonation - Enables user impersonation. False by default.
- (OPTIONAL) - silentAuth_ - A hash of options for configuring Silent Authentication -- see the linked doc section for more details.
An example configuration might look something like:
// config/environment.js
module.exports = function(environment) {
let ENV = {
'ember-simple-auth': {
authenticationRoute: 'login',
auth0: {
clientID: '<client_id>',
domain: '<your_domain>.auth0.com',
logoutReturnToURL: '/logout',
enableImpersonation: false,
silentAuth: {
// Silent authentication is off by default.
// See 'Silent Authentication' section in this
// readme for a list of options that go here.
}
}
}
};
return ENV;
};
If you are using content security policy to manage which resources are allowed to be run on your pages, add the following CSP rules:
// config/environment.js
ENV.contentSecurityPolicy = {
'font-src': "'self' data: https://*.auth0.com",
'style-src': "'self' 'unsafe-inline'",
'script-src': "'self' 'unsafe-eval' https://*.auth0.com",
'img-src': '*.gravatar.com *.wp.com data:',
'connect-src': "'self' http://localhost:* https://your-app-domain.auth0.com"
};
In your application route, be sure to import ApplicationRouteMixin from this addon (i.e. not the one that ships with Ember Simple Auth), or else things like session expiration will not work correctly.
// app/routes/application.js
import Route from '@ember/routing/route';
import RSVP from 'rsvp';
import ApplicationRouteMixin from 'ember-simple-auth-auth0/mixins/application-route-mixin';
export default Route.extend(ApplicationRouteMixin, {
beforeSessionExpired() {
// Do custom async logic here, e.g. notify
// the user that they are about to be logged out.
return RSVP.resolve();
}
// Do other application route stuff here. All hooks provided by
// ember-simple-auth's ApplicationRouteMixin, e.g. sessionInvalidated(),
// are supported and work just as they do in basic ember-simple-auth.
});
To use the embedded Lock widget, in your application controller, or wherever else you wish to do authentication (e.g. a '/login' route+controller), inject the session service and use the auth0-lock
authenticator, like so:
// app/controllers/application.js
import Controller from '@ember/controller';
import { inject as service } from '@ember/service';
export default Controller.extend({
session: service(),
actions: {
login () {
// Check out Auth0 Lock's documentation for all the options:
// https://auth0.com/docs/libraries/lock/customization
const lockOptions = {
auth: {
params: {
scope: 'openid email profile'
}
}
};
this.session.authenticate('authenticator:auth0-lock', lockOptions);
},
logout () {
this.session.invalidate();
}
}
});
When the login
action above is fired, the Lock widget is created using the options passed to the authenticate
function. Refer to Auth0's documentation for notes on how to set up Lock itself -- all options are passed through to Lock as-is.
To perform passwordless login, use the auth0-lock-passwordless
authenticator. That's it!
For more information on how to set up Passwordless authentication server side and how to configure the Lock, see the following official guides:
- Using Passwordless Authentication (server-side setup)
- Passwordless Options for Lock
An example might look like this:
// app/controllers/application.js
import Controller from '@ember/controller';
import { inject as service } from '@ember/service';
export default Controller.extend({
session: service(),
actions: {
login () {
// Check out the docs for all the options:
// https://github.com/auth0/lock-passwordless#customization
const lockOptions = {
allowedConnections: ['email'],
passwordlessMethod: 'link',
authParams: {
scope: 'openid email profile'
}
};
this.session.authenticate('authenticator:auth0-lock-passwordless', lockOptions, (err, email) => {
console.log(`Email link sent to ${email}!`)
});
},
logout () {
this.session.invalidate();
}
}
});
Note that you can pass in a callback as the last argument to handle events after a passwordless link has been sent.
To use Auth0's Universal Login workflow (i.e. an Auth0-hosted login page), use the auth0-universal
authenticator. This will redirect the user to the hosted login page (just be sure to set this up on the server through your Auth0 dashboard first).
Behind the scenes, the authenticator calls Auth0.js's authorize method, so see the linked docs for a full list of supported options.
An example:
// app/controllers/application.js
import Controller from '@ember/controller';
import { inject as service } from '@ember/service';
export default Controller.extend({
session: service(),
actions: {
login () {
// Check out the docs for all the options:
// https://auth0.com/docs/libraries/auth0js/v9#webauth-authorize-
const authOptions = {
responseType: 'token',
scope: 'openid email profile'
};
this.session.authenticate('authenticator:auth0-universal', authOptions, (err, email) => {
console.log(`Email link sent to ${email}!`)
});
},
logout () {
this.session.invalidate();
}
}
});
This addon contains native impersonation support. Just follow the instructions on Auth0's documentation and you will be logged in.
Note that before you can use impersonation, you must enable it in your app configuration -- see the Configuration section above.
The new session object will include the following fields:
{
"authenticated": {
"authenticator": "authenticator:auth0-url-hash",
//...
"profile": {
"impersonated": true,
"impersonator": {
"user_id": "google-oauth2|108251222085688410292",
"email": "[email protected]"
}
}
//...
}
}
Since version 4.2.0, this addon supports automatic Silent Authentication, a.k.a. the ability to automatically refresh session tokens upon (or before) expiration.
Automatic silent authentication enabled in the app's environment configuration file; next to the rest of thea auth0 config options, simply provide a silentAuth
object with the following:
- (OPTIONAL) - renewSeconds - If set, the token will be renewed on a timer, every specified number of seconds.
- (OPTIONAL) - onSessionRestore - If
true
, the token will be renewed when trying to restore an expired session token on app load. - (OPTIONAL) - onSessionExpire - If
true
, the token will be renewed when the active session token expires during app use. - (REQUIRED) - options - A hash of options to pass to checkSession, the function which performs Silent Authentication behind the scenes. See linked docs for details on what these options can be.
Although the first 3 parameters are technically optional, at least one of them needs to be set for anything to happen, naturally.
A typical example might look like the following:
// config/environment.js
module.exports = function(environment) {
let ENV = {
'ember-simple-auth': {
// ...
auth0: {
// ...
silentAuth: {
// automatically renew token every 30 minutes:
renewSeconds: 1800,
// automatically renew token when trying to restore an expired session (on app load):
onSessionRestore: true,
// automatically renew token when token expiration time is hit (during app use):
onSessionExpire: true,
// options to pass to checkSession when doing automatic silent auth.
// The redirectUri parameter is automatically set to window.location.origin
// if not specified.
options: {
responseType: 'token id_token',
scope: 'openid profile email',
timeout: 5000
}
}
}
}
};
return ENV;
};
In addition to the above, an auth0-silent-auth
authenticator is provided in case you have a particular custom hook in your application you wish to trigger a token refresh from, but this is a rather advanced use case that most users won't need to mess with.
After the user has been authenticated, session.data.authenticated
is filled with the data returned by Auth0. What gets stored here is dependent on the scope
property in your authentication options; for instance, this is what the session object looks like with scope
set to "openid email profile" (sans the placeholders in <angle brackets>, which are filled with real data during actual use):
Note: all keys coming back from auth0 are transformed to camelcase for consistency
{
"authenticated": {
"authenticator": "authenticator:auth0-lock",
"accessToken": "<access_token>",
"idToken": "<id_token>",
"idTokenPayload": {
"iss": "https://<your_domain>.auth0.com/",
"sub": "auth0|<user_id>",
"aud": "<client_id>",
"iat": 1521131759,
"exp": 1521167759
},
"appState": null,
"refreshToken": null,
"state": "<state>",
"expiresIn": 86400,
"tokenType": "Bearer",
"scope": "openid email profile",
"profile": {
"email": "[email protected]",
"picture": "https://s.gravatar.com/avatar/aaafe9b3923266eacb178826a65e92d1?s=480&r=pg&d=https%3A%2F%2Fcdn.auth0.com%2Favatar2%2Fcw.png",
"nickname": "bob.johnson",
"name": "[email protected]",
"last_password_reset": "2018-03-11T18:03:13.291Z",
"email_verified": true,
"user_id": "auth0|<user_id>",
"clientID": "<client_id>",
"identities": [
{
"user_id": "<user_id>",
"provider": "auth0",
"connection": "<connection_id>",
"isSocial": false
}
],
"updated_at": "2018-03-15T16:35:59.036Z",
"created_at": "2016-11-09T22:43:53.994Z",
"sub": "auth0|<user_id>"
}
}
}
You can use this in your templates that have the session service injected, like so:
Errors come back as a hash in the URL. These will be automatically parsed and Ember will transition to the error route with two variables set on the model: error
and errorDescription
. A quick example:
ember g template application-error
The plugin ember-simple-auth
provides the authorize
hook to add the token of the user to the headers of the API request.
See server for an example of an express application getting called by the ember app.
An example using ember-data:
ember g adapter application
import JSONAPIAdapter from 'ember-data/adapters/json-api';
import DataAdapterMixin from 'ember-simple-auth/mixins/data-adapter-mixin';
import { isPresent } from '@ember/utils';
import { debug } from '@ember/debug';
export default JSONAPIAdapter.extend(DataAdapterMixin, {
authorize(xhr){
const { idToken } = this.get('session.data.authenticated');
if (isPresent(idToken)) {
xhr.setRequestHeader('Authorization', `Bearer ${idToken}`);
} else {
debug('Could not find the authorization token in the session data.');
}
}
});
// app/routes/application.js
import Route from '@ember/routing/route';
import ApplicationRouteMixin from 'ember-simple-auth-auth0/mixins/application-route-mixin';
export default Route.extend(ApplicationRouteMixin, {
model() {
return this.store.findAll('my-model');
}
});
This will make the following request
GET
http://localhost:4200/my-model
Accept: application/vdn+json-api
Authorization: Bearer 123.123123.1231
To make an API request without ember-data, add the user's JWT token to an Authorization
HTTP header:
fetch('/api/foo', {
method: 'GET',
cache: false,
headers: {
'Authorization': `Bearer ${session.data.authenticated.jwt}`
}
}).then(function (response) {
// use response
});
The major breaking change in 5.x is the removal of the jwt
authorizer. Ember Simple Auth has deprecated authorizers and will be removing them in a future release, so this addon has followed suit for futureproofing's sake.
If you're directly using the jwt
authorizer through the session service, like so:
// app/controllers/something.js
import Controller from '@ember/controller';
import { inject as service } from '@ember/service';
export default Controller.extend({
session: service(),
actions: {
doSomething () {
// ...
this.session.authorize('authorizer:jwt', (headerName, headerValue) => {
// ...do something with the header.
});
// ...
}
}
});
Either construct an Authentication header from session.data.authenticated
as shown in the Calling an API guide above, or just inject the auth0
service and call the authorize
method, like so:
// app/controllers/something.js
import Controller from '@ember/controller';
import { inject as service } from '@ember/service';
export default Controller.extend({
auth0: service(),
actions: {
doSomething () {
// ...
this.auth0.authorize((headerName, headerValue) => {
// ...do something with the header.
});
// ...
}
}
});
The auth0.authorize
method is nearly the same as session.authorize
, but there's one less parameter since you no longer have to specify an authorizer type.
If you're currently Ember Simple Auth's DataAdapterMixin along with the jwt
authorizer to make ember-data work, this addon includes a replacement Auth0DataAdapterMixin that does this for you:
// app/adapters/application.js
import JSONAPIAdapter from 'ember-data/adapters/json-api';
import Auth0DataAdapterMixin from 'ember-simple-auth-auth0/mixins/auth0-data-adapter-mixin';
export default JSONAPIAdapter.extend(Auth0DataAdapterMixin, {
// customizer your adpater further here, if you wish.
});
Note that this is functionally equivalent to customizing the adapter as shown in the Calling an API guide above. The guides in the main sections of this README use the methodology recommended by Ember Simple Auth (that is, constructing a header directly) rather than use these shortcut functions/mixins, but they're effectively the same. It's a matter of taste and convenience, mostly.
Starting from version 4.0.0, this addon uses Lock v11, which now supports Passwordless functionality among other things. As such, there are a few breaking changes to consider for users coming from v3.x
First and foremost, take a look at the following guides from Auth0; these cover most of the requirements:
- Migrating from Lock v10 to v11
- Migration Guide for lock-passwordless to Lock v11 with Passwordless Mode
For those using this addon with Passwordless authentication, the API for the
auth0-lock-passwordless
authenticator has changed.
The major breaking change is that the "type" parameter for the auth0-lock-passwordless
authenticator is gone. Instead, set the passwordlessMethod
and allowedConnections
options
in the options hash:
// app/controllers/application.js
import Controller from '@ember/controller';
import { inject as service } from '@ember/service';
export default Controller.extend({
session: service(),
actions: {
// OLD method of invoking passwordless auth (v3.x):
loginOld () {
const lockOptions = {
authParams: {
scope: 'openid email profile'
}
};
this.session.authenticate('authenticator:auth0-lock-passwordless', 'magiclink', lockOptions, (err, email) => {
console.log(`Email link sent to ${email}!`)
});
},
// NEW method of invoking passwordless auth (v4.x):
loginNew () {
const lockOptions = {
allowedConnections: ['email'],
passwordlessMethod: 'link',
authParams: {
scope: 'openid email profile'
}
};
this.session.authenticate('authenticator:auth0-lock-passwordless', lockOptions, (err, email) => {
console.log(`Email link sent to ${email}!`)
});
},
logout () {
this.session.invalidate();
}
}
});
The good news here is that the auth0-lock-passwordless
authenticator works exactly
like auth0-lock
; no more subtle differences.
On the off-chance your Ember app is calling the showPasswordlessLock
method of the
auth0
service directly, its type
parameter has similarly been removed. The
process of converting type
to options
is the same as above.
See the Initialization options section of Auth0's Passwordless migration guide for more details, though the above advice should hopefully suffice.
User impersonation is disabled by default
in newer versions of Auth0.js (and consequently, this addon starting from v4.0.0). To
enable it, you'll need to set the enableImpersonation
flag in your app's
config/environment.js
, like so:
// config/environment.js
module.exports = function(environment) {
let ENV = {
'ember-simple-auth': {
authenticationRoute: 'login',
auth0: {
clientID: '1234',
domain: 'my-company.auth0.com',
logoutReturnToURL: '/logout',
enableImpersonation: true
}
}
};
return ENV;
};
Be warned that enabling impersonation has security trade-offs, so use with caution.
If you want to craft acceptance tests for Auth0's Lock, there are two things you can do:
- If you are just using the default auth0-lock authenticator then all you have to do is authenticateSession.
- If you are manually invoking the auth0 lock you should use the
showLock
function on the auth0 service and then callmockAuth0Lock
in your test.
// tests/acceptance/login.js
import { module, test } from 'qunit';
import { setupApplicationTest } from 'ember-qunit';
import { visit, currentURL } from '@ember/test-helpers';
import { mockAuth0Lock } from 'ember-simple-auth-auth0/test-support';
import { authenticateSession, currentSession } from 'ember-simple-auth/test-support';
module('Acceptance | login', function(hooks) {
setupApplicationTest(hooks);
test('visiting /login redirects to /protected page if authenticated', async function(assert) {
assert.expect(1);
const sessionData = {
idToken: 1
};
await authenticateSession(sessionData);
await visit('/login');
let session = currentSession(this.application);
let idToken = get(session, 'data.authenticated.idToken');
assert.equal(idToken, sessionData.idToken);
assert.equal(currentURL(), '/protected');
});
test('it mocks the auth0 lock login and logs in the user', async function(assert) {
assert.expect(1);
const sessionData = {
idToken: 1
};
await mockAuth0Lock(sessionData);
await visit('/login');
assert.equal(currentURL(), '/protected');
});
});
If you want to replace the authenticator (e.g. for testing purposes), here is a
minimal example. The mock JWT in this example is in window.mockJwt
and is
generated by the backend in a fullstack testing environment.
import { resolve, Promise } from 'rsvp';
import Base from 'ember-simple-auth/authenticators/base';
export default Base.extend({
restore(data) {
return resolve(data);
},
authenticate() {
return new Promise((res) => {
const idToken = window.mockJwt;
const sessionData = {
idToken,
expiresIn: 60 * 60, // one hour is more than enough for one test case
idTokenPayload: {
// 'iat' is short for 'issued at' in seconds
iat: Math.ceil(Date.now() / 1000),
}
};
res(sessionData);
});
},
});
The application route mixin of this
plugin expects the two values idTokenPayload.iat
and expiresIn
to be present
in the session data. If you don't provide these two values, your session will
expire immediately.
git clone
this repositorycd ember-simple-auth-auth0
npm install
- Set the environment variable
AUTH0_CLIENT_ID_ID={Your account id}
- Set the environment variable
AUTH0_DOMAIN={Your account domain}
- Grab from your those from the Auth0 Dashboard
ember serve
- Visit your app at http://localhost:4200.
npm run lint:js
npm run lint:js -- --fix
ember test
– Runs the test suite on the current Ember versionember test --server
– Runs the test suite in "watch mode"ember try:each
– Runs the test suite against multiple Ember versions
For more information on using ember-cli, visit https://ember-cli.com/.
This project is licensed under the MIT License.