Ember Authentication
Using https://ember-simple-auth.com/ with OpenID Connect (oidc
)
and Django OpenID Connect
These notes link to the Mainmatter YouTube video, https://www.youtube.com/watch?v=bSWN4_EbTPI
Protect a Route
To protect a route, inject the session service and
then requireAuthentication
:
import { inject as service } from "@ember/service"
export default class AppsRoute extends Route {
@service session
async beforeModel(transition) {
// if not authenticated, transition to 'authenticate'
this.session.requireAuthentication(transition, "authenticate")
Note
We pass the transition
to requireAuthentication
so
the authentication service knows where to go
after a successful login
If you want to transition to the login
route:
import { getOwner } from "@ember/application";
import { service } from '@ember/service';
export default class AppsRoute extends Route {
@service session
async beforeModel(transition) {
// if not authenticated, transition to 'login'
const config = getOwner(this).resolveRegistration("config:environment");
let loginRoute = config.APP.loginRoute;
this.session.requireAuthentication(transition, loginRoute)
}
Permissions
Each project will have it’s own front/app/utils/
folder containing
functions e.g:
is-app-admin-workflow.js
is-app-user-workflow.js
Here is an example function:
export default function isAppAdminWorkflow(contact) {
let isAppAdmin = contact && contact.isAppAdminWorkflow;
let isSuperuser = contact && contact.isSuperuser;
return isSuperuser || isAppAdmin;
}
e.g:
import isAppAdminWorkflow from '../utils/is-app-admin-workflow';
if (isAppAdminWorkflow(contact)) {
Template
isAuthenticated
{{#if this.session.isAuthenticated}}
Tip
Inject @service session
into the route, controller or component.
API
To use the token in API requests, see:
app/adapters/application.js
in the project
Logout
To logout, invalidate the session:
import { inject as service } from '@ember/service';
@service session;
@action
logout() {
this.session.invalidate();
Tip
See below for more information on this.session.invalidate
.
Configuration
Your login
route will be:
login-oidc
if using OIDClogin-pass
if using user name / password.
Environment
Tip
Source code in config/environment.js
for the project.
module.exports = function (environment) {
let ENV = {
APP: {
loginRoute: "login-oidc",
Tip
The loginRoute
(in the APP
section) will be either
login-oidc
or login-pass
.
If using OIDC, configure the ember-simple-auth
and oidcAuth
sections:
module.exports = function (environment) {
let ENV = {
'ember-simple-auth': {
routeAfterAuthentication: 'apps',
},
oidcAuth: {
afterLogoutUri: 'https://myember.hatherleigh.info/',
authEndPoint: '/authorize',
clientId: 'a100bc-2def-3egh',
host: 'https://login.microsoftonline.com/1234-a123-4567/oauth2/v2.0',
redirectEndPoint: "login-oidc",
tokenEndPoint:
'https://myember.hatherleigh.info/back/token/',
},
Router
Tip
Source code in app/router.js
for the project.
Set login-oidc
or login-pass
as your route,
then set the path
to /login
e.g:
Router.map(function () {
this.route('login-oidc', { path: '/login' });
Initialise
setup
the session in front/app/routes/application.js
:
import { inject as service } from '@ember/service';
export default class ApplicationRoute extends Route {
@service intl;
@service session;
async beforeModel() {
await this.session.setup();
this.intl.setLocale(['en-uk']);
Current Contact
From Managing a Current User. Also see Current Contact (below)…
Create front/app/services/session.js
:
import { inject } from '@ember/service';
import BaseSessionService from 'ember-simple-auth/services/session';
export default class SessionService extends BaseSessionService {
@inject currentContact;
async handleAuthentication() {
super.handleAuthentication(...arguments);
try {
await this.currentContact.load();
} catch (err) {
await this.invalidate();
}
}
}
Note
I don’t know how / why this works as the session
is injected into
views etc, but this session
has the same name as the default one.
If I move this module to ember-kb-base
it does not work.
Load the current contact in front/app/routes/application.js
:
import { inject as service } from '@ember/service';
export default class ApplicationRoute extends Route {
@service currentContact;
async beforeModel() {
await this.session.setup();
this.intl.setLocale(['en-uk']);
return this._loadCurrentContact();
}
async _loadCurrentContact() {
try {
await this.currentContact.load();
} catch (err) {
await this.session.invalidate();
}
if (!this.currentContact.contactId) {
await this.session.invalidate();
}
}
}
How does it work?
Route - authenticate
Tip
The source code is in ember-kb-base/src/routes/authenticate.js
The authenticate
route displays a link to the login
route.
Note
A failed login cannot redirect to the login
route because we
could end up with an endless loop of logins
(assuming they fail every time).
The beforeModel
method in the login
route to checks to see if the
session is authenticated and if it is, transitions to the specified route:
this.session.prohibitAuthentication(simpleAuth.routeAfterAuthentication);
Note
routeAfterAuthentication
will typically be set to a dashboard or
landing page e.g. apps
.
Route - login-oidc
Tip
The source code is in ember-kb-base/src/routes/login-oidc.js
.
The afterModel
method in the login-oidc
route calls the
_handleRedirectRequest
method.
The _handleRedirectRequest
method redirects to the OIDC host e.g.
https://login.microsoftonline.com/1234abc-d123-4321/oauth2/v2.0/authorize
Note
The redirect_uri
for the OIDC host is set to the login
route
(i.e. this route).
After successful authentication with the OIDC host, we are redirected back to
the login
route (via the redirect_uri
) with a code
in the query
parameters.
The afterModel
method in the login
route calls the
_handleCallbackRequest
method.
The _handleCallbackRequest
method calls the session authenticate
method which calls the authenticate
method in the oidc
authenticator
Authenticator - oidc
Tip
the source code is in ember-kb-base/src/authenticators/oidc.js
authenticate
The
authenticate
method in the oidc authenticator does aPOST
request to the Django URL (tokenEndPoint
) passing thecode
(seeredirect_uri
above).Django authenticates the user (OpenID Connect) and returns a
token
and the ID of the contact (contactId
).The data returned by the
authenticate
method (token
andcontactId
) is stored in the ember simple auth session data
invalidate
Once the session is invalidated (on Logout) the authenticated data is cleared.
this.session.invalidate
will call this invalidate method on the authenticator class. In most cases, nothing needs to be done here.
restore
The restore method on the authenticator class restores the session from the session store (see Session Reload).
If required, this method could be used to restore an expired token.
Session Reload
To preserve session data across page reloads, ember-simple-auth
will use
the AdaptiveStore by default.
Tip
To use Ember Simple Auth with FastBoot, configure the CookieStore as the application session store.
Current Contact
The source code for the currentContact
service is in
ember-kb-base/src/services/current-contact.js
Permissions
For now…
The back.serializers.ContactSerializer
returns is_app_administrator
and is_manager
:
is_app_administrator
returnsTrue
if the user is the manager of theApp
hard-coded into theContactSerializer
.is_manager
checks thedefault_department_manager
and thedefault_terminal_manager
.
Warning
is_app_administrator
will not be useful if the Ember app
is for more than one App
.
The ContactModel
(app/models/contact.js
) has a permissionsLevel
method which returns appadmin
or manager
if the user
is_app_administrator
or is_manager
.
The appMenu
has an authentication
attribute which lists the required
levels i.e. false
(no authentication), authenticated
, manager
or
appadmin
.
The authentication
attribute is used by the eyes-only helper e.g:
{{#each @appMenu as |menu|}}
{{#if
(eyes-only
menu.authentication
@isAuthenticated
@currentContact.contact.permissionLevel
)
}}
Tip
The source code is here, eyes-only …