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.jsin 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-oidcif using OIDClogin-passif 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
authenticatemethod in the oidc authenticator does aPOSTrequest to the Django URL (tokenEndPoint) passing thecode(seeredirect_uriabove).Django authenticates the user (OpenID Connect) and returns a
tokenand the ID of the contact (contactId).The data returned by the
authenticatemethod (tokenandcontactId) is stored in the ember simple auth session data
invalidate
Once the session is invalidated (on Logout) the authenticated data is cleared.
this.session.invalidatewill 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_administratorreturnsTrueif the user is the manager of theApphard-coded into theContactSerializer.is_managerchecks thedefault_department_managerand 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 …