chore: add core deauthenticate flow
This commit is contained in:
8 files changed
+152
-48
No files matched your search
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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(
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user