Ember Authentication ******************** - :doc:`dev-ember` - :doc:`dev-ember-addons-kb` - :doc:`dev-ember-addons` - :doc:`dev-ember-auth` - :doc:`dev-ember-data` - :doc:`dev-ember-patterns` Using https://ember-simple-auth.com/ with OpenID Connect (``oidc``) and Django :ref:`app_login_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) } Template ======== ``isAuthenticated`` ------------------- .. code-block:: html {{#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 - DataAdapterMixin_ - `Ember Simple Auth ref Adapter`_ 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 (:ref:`app_login_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`_ ... .. _`after a successful login`: https://youtu.be/bSWN4_EbTPI?t=514 .. _`configure the CookieStore`: https://youtu.be/bSWN4_EbTPI?t=906 .. _`Ember Simple Auth ref Adapter`: https://youtu.be/bSWN4_EbTPI?t=770 .. _`ember simple auth session data`: https://youtu.be/bSWN4_EbTPI?t=450 .. _`eyes-only`: https://gitlab.com/kb/kb-base-ember-addons/-/blob/master/addon/helpers/eyes-only.js?ref_type=heads .. _`invalidate method on the authenticator`: https://youtu.be/bSWN4_EbTPI?t=665 .. _`invalidate the session`: https://youtu.be/bSWN4_EbTPI?t=617 .. _`Managing a Current User`: https://github.com/mainmatter/ember-simple-auth/blob/master/guides/managing-current-user.md .. _`oidc authenticator`: https://youtu.be/bSWN4_EbTPI?t=344 .. _`restore method on the authenticator`: https://youtu.be/bSWN4_EbTPI?t=964 .. _`session authenticate`: https://youtu.be/bSWN4_EbTPI?t=232 .. _`session service`: https://www.youtube.com/watch?v=bSWN4_EbTPI&t=100s .. _AdaptiveStore: http://ember-simple-auth.com/api/classes/AdaptiveStore.html .. _DataAdapterMixin: https://ember-simple-auth.com/api/classes/DataAdapterMixin.html