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:

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 OIDC

  • login-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 a POST request to the Django URL (tokenEndPoint) passing the code (see redirect_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 and contactId ) 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

From Managing a Current User

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 returns True if the user is the manager of the App hard-coded into the ContactSerializer.

  • is_manager checks the default_department_manager and the default_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