ember-simple-auth-auth0
v5.0.0-beta.0
Published
ember-simple-auth addon for Auth0 + Lock.js.
Downloads
165
Readme
ember-simple-auth-auth0
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.
Table of Contents
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
Basic Information
What does it do?
- 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!
Example App
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.
Installation
Auth0 Setup
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!
Addon Installation
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!
Configuration
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"
};
Application Route Setup
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.
});
Usage
Embedded Lock
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();
}
}
});
{{!-- app/templates/application.hbs --}}
{{#if session.isAuthenticated}}
<div>
You are currently logged as: {{session.data.authenticated.profile.email}}
</div>
<a href="" {{ action "logout" }}>Logout</a>
{{else}}
<a href="" {{ action "login" }}>Login</a>
{{/if}}
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.
Passwordless Login
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.
Universal Login
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();
}
}
});
Feature Guides
Impersonation
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]"
}
}
//...
}
}
Silent Authentication
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.
Session Data
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:
My logged in user email is {{session.data.authenticated.profile.email}}!
Handling Errors
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
// app/templates/application-error.hbs
Encountered an error from auth0 - {{model.error}} -- {{model.errorDescription}}
Calling an API
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
});
Migration Guides
Migrating from ember-simple-auth-auth0 v4.x to v5.x
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.
Replace JWT Authorizer
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.
Replace DataAdapterMixin
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.
Migrating from ember-simple-auth-auth0 v3.x to v4.x
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
Auth0 Migration Guides
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
Passwordless Auth Changes
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.
Impersonation Changes
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.
Developing this Addon
Acceptance Testing
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');
});
});
Stubbing out the authenticator for testing purposes
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.
Contributing
Cloning
git clone
this repositorycd ember-simple-auth-auth0
npm install
Running
- 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.
Linting
npm run lint:js
npm run lint:js -- --fix
Running tests
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/.
License
This project is licensed under the MIT License.