chore: add core deauthenticate flow

This commit is contained in:
Randall Morey committed 2020-07-24 17:49:42 -04:00
1 parent 99b5290570
commit 659a494092
8 files changed
+152 -48

No files matched your search

+10
View File
@@ -0,0 +1,10 @@
import Controller from '@ember/controller';
import { inject as service } from '@ember/service';
export default class ApplicationController extends Controller {
// =services
@service session;
}
+63 -1
View File
@@ -1,6 +1,68 @@
import Route from '@ember/routing/route';
import ApplicationRouteMixin from 'ember-simple-auth/mixins/application-route-mixin';
import Ember from 'ember';
import { getOwner } from '@ember/application';
import { inject as service } from '@ember/service';
import { action } from '@ember/object';
import { later } from '@ember/runloop';
/**
* Entry route for the application.
*/
export default class ApplicationRoute extends Route {}
export default class ApplicationRoute extends Route.extend(
ApplicationRouteMixin
) {
// =services
@service session;
// =attributes
routeIfUnauthenticated = 'index';
// =methods
/**
* After becoming authenticated, does nothing. This overrides the default
* behavior of the ApplicationRouteMixin, which is to redirect after auth.
* We'll handle this redirect manually in sub routes.
* @override
*/
sessionAuthenticated() {
// no op
}
/**
* When the session ends, redirect to authenticate and reload the page to
* purge any in-memory state.
*
* TODO This application route is redirecting to a subroute, assuming
* that the user is logged into a specific organization. This won't always
* necessarily be the case. When we support MSP, users may authenticate
* without an org scope. So this will need to be rethought.
*/
async sessionInvalidated() {
// Catch error in this transition, since it will be aborted by the
// org auth route when it redirects to the first auth method.
await this.transitionTo(this.routeIfUnauthenticated);
// The Ember way of accessing globals...
const document = getOwner(this).lookup('service:-document').documentElement;
// defaultView === window, but without using globals directly
const { location } = document.parentNode.defaultView;
// Wait a beat, then reload the page...
// This is mostly to give the deauth request a chance to fire.
/* istanbul ignore if */
if (!Ember.testing) later(location, location.reload, 150);
}
// =actions
/**
* Delegates invalidation to the session service.
*/
@action
invalidateSession() {
this.session.invalidate();
}
}
@@ -44,7 +44,6 @@ export default class ScopesScopeAuthenticateMethodRoute extends Route {
const authenticatorName = 'authenticator:password';
const scope = this.modelFor('scopes.scope');
const authMethod = this.currentModel;
const authMethodId = authMethod.id;
const requestCookies = false;
try {
await this.session.authenticate(
+18 -10
View File
@@ -6,6 +6,8 @@
<layout.header>
<Rose::Header as |header|>
<header.brand @logo="logo-hashi" @text={{app-name}} />
{{!--
<header.nav as |nav|>
<nav.dropdown @text={{t 'dev.default-org'}} as |dropdown|>
<dropdown.link @route="index">{{t 'dev.menu-item'}}</dropdown.link>
@@ -15,16 +17,22 @@
<nav.link @route="index">{{t 'dev.section'}}</nav.link>
<nav.link @route="index">{{t 'dev.section'}}</nav.link>
</header.nav>
<header.utilities as |utilities|>
<utilities.dropdown
@text="user@example.net"
@iconOnly={{true}}
@icon="user-square-fill" as |dropdown|
>
<dropdown.link @route="index">{{t 'dev.menu-item'}}</dropdown.link>
<dropdown.button>{{t 'dev.button-menu-item'}}</dropdown.button>
</utilities.dropdown>
</header.utilities>
--}}
{{#if this.session.isAuthenticated}}
<header.utilities as |utilities|>
<utilities.dropdown
@text="user@example.net"
@iconOnly={{true}}
@icon="user-square-fill" as |dropdown|
>
<dropdown.button {{on "click" (route-action "invalidateSession")}}>
{{t 'actions.deauthenticate'}}
</dropdown.button>
</utilities.dropdown>
</header.utilities>
{{/if}}
</Rose::Header>
</layout.header>
+3 -2
View File
@@ -1,5 +1,5 @@
import config from '../config/environment';
import { authHandler } from './route-handlers/auth';
import { authHandler, deauthHandler } from './route-handlers/auth';
import { Response } from 'miragejs';
export default function() {
@@ -56,8 +56,9 @@ export default function() {
this.patch('/scopes/:scope_id/auth-methods/:id');
this.del('/scopes/:scope_id/auth-methods/:id');
// Authenticate/deauthenticate route
// Authenticate/deauthenticate routes
this.post('/scopes/:scope_id/auth-methods/:id_method', authHandler);
this.post('/scopes/:id_method', deauthHandler);
// IAM : Users
this.get('/scopes/:scope_id/users');
+20 -30
View File
@@ -23,38 +23,28 @@ import { Response } from 'miragejs';
// }
// }
/**
* Mirage doesn't distinguish between `/path:authenticate` and
* `/path:deauthenticate`, so we use the presence of a request body to indicate
* an authentication request and its absence to indicate deauthentication.
*/
export function authHandler({ scopes }, request) {
const [ , method ] = request.params.id_method.split(':');
const payload = JSON.parse(request.requestBody);
const isAuthenticationRequest = method === 'authenticate';
if (isAuthenticationRequest) {
// this is an auth request
if (payload.credentials.name === 'error') {
return new Response(400);
} else {
console.log(request);
const scopeAttrs =
this.serialize(scopes.find(request.params.scope_id));
const scope = scopeAttrs;
return new Response(200, {}, {
scope,
id: 'token123',
token: 'thetokenstring',
user_id: 'user123',
auth_method_id: 'authmethod123',
created_time: '',
updated_time: '',
last_used_time: '',
expiration_time: ''
});
}
if (payload.credentials.name === 'error') {
return new Response(400);
} else {
// this is a deauth request
return new Response(200);
const scopeAttrs =
this.serialize(scopes.find(request.params.scope_id));
const scope = scopeAttrs;
return new Response(200, {}, {
scope,
id: 'token123',
token: 'thetokenstring',
user_id: 'user123',
auth_method_id: 'authmethod123',
created_time: '',
updated_time: '',
last_used_time: '',
expiration_time: ''
});
}
}
export function deauthHandler() {
return new Response(200);
}
@@ -3,6 +3,11 @@ import { visit, currentURL, fillIn, click, find } from '@ember/test-helpers';
import { setupApplicationTest } from 'ember-qunit';
import setupMirage from 'ember-cli-mirage/test-support/setup-mirage';
import a11yAudit from 'ember-a11y-testing/test-support/audit';
import {
currentSession,
//authenticateSession,
//invalidateSession,
} from 'ember-simple-auth/test-support';
module('Acceptance | authentication', function(hooks) {
setupApplicationTest(hooks);
@@ -10,6 +15,7 @@ module('Acceptance | authentication', function(hooks) {
let orgScope;
let orgScopeID;
let scope;
let authMethod;
let authMethodID;
let authMethodAuthenticateURL;
@@ -17,8 +23,8 @@ module('Acceptance | authentication', function(hooks) {
hooks.beforeEach(function () {
orgScope = this.server.create('scope', {type: 'org'}, 'withChildren');
const scope = { id: orgScope.id, type: orgScope.type };
authMethod = server.create('auth-method', { scope });
scope = { id: orgScope.id, type: orgScope.type };
authMethod = this.server.create('auth-method', { scope });
orgScopeID = orgScope.id;
authMethodID = authMethod.id;
authMethodAuthenticateURL = `/scopes/${orgScopeID}/authenticate/${authMethodID}`;
@@ -33,21 +39,37 @@ module('Acceptance | authentication', function(hooks) {
});
test('failed authentication shows a notification message', async function(assert) {
assert.expect(1);
assert.expect(3);
await visit(authMethodAuthenticateURL);
assert.notOk(currentSession().isAuthenticated);
await fillIn('[name="identification"]', 'error');
await click('[type="submit"]');
assert.ok(find('.rose-notification.is-error'));
assert.notOk(currentSession().isAuthenticated);
});
test('successful authentication redirects to projects', async function(assert) {
assert.expect(2);
assert.expect(4);
await visit(authMethodAuthenticateURL);
assert.notOk(currentSession().isAuthenticated);
assert.equal(currentURL(), authMethodAuthenticateURL);
await fillIn('[name="identification"]', 'test');
await fillIn('[name="password"]', 'test');
await click('[type="submit"]');
assert.equal(currentURL(), projectsURL);
assert.ok(currentSession().isAuthenticated);
});
test('deauthentication redirects to index', async function(assert) {
assert.expect(3);
await visit(authMethodAuthenticateURL);
await fillIn('[name="identification"]', 'test');
await fillIn('[name="password"]', 'test');
await click('[type="submit"]');
assert.ok(currentSession().isAuthenticated);
await click('.rose-button-header-dropdown');
assert.notOk(currentSession().isAuthenticated);
assert.equal(currentURL(), '/');
});
});
@@ -0,0 +1,12 @@
import { module, test } from 'qunit';
import { setupTest } from 'ember-qunit';
module('Unit | Controller | application', function(hooks) {
setupTest(hooks);
// Replace this with your real tests.
test('it exists', function(assert) {
let controller = this.owner.lookup('controller:application');
assert.ok(controller);
});
});