feature: initial OIDC authentication flow

This commit is contained in:
Randall Morey committed 2021-01-22 17:01:12 -05:00
1 parent ac92575b36
commit 3084d6cf06
7 files changed
+201 -31

No files matched your search

+26 -1
View File
@@ -23,6 +23,12 @@ import { Response } from 'miragejs';
// }
// }
// Quick and dirty counter system to simulate the polling behavior
// of the API. If the OIDC flow is pending, the route returns 100.
// If complete it returns 200 with the token JSON.
let oidcAttemptCounter = 0;
const oidcRequiredAttempts = 3;
const methodHandlers = {
password: {
authenticate: (payload, scopeAttrs) => {
@@ -44,6 +50,7 @@ const methodHandlers = {
}
}
},
/**
* OIDC authentication is a two-step process:
*
@@ -62,7 +69,25 @@ const methodHandlers = {
token_request_id: 'token_request_1234',
state: 'base_58_encoded_string'
}),
authenticate: () => new Response(200) // TODO
authenticate: (_, scopeAttrs) => {
oidcAttemptCounter++;
if (oidcAttemptCounter < oidcRequiredAttempts) {
return new Response(100);
} else {
return new Response(200, {}, {
scope: scopeAttrs,
id: 'token123',
token: 'thetokenstring',
account_id: '1',
user_id: 'user123',
auth_method_id: 'authmethod123',
created_time: '',
updated_time: '',
last_used_time: '',
expiration_time: ''
});
}
}
}
};
+62 -25
View File
@@ -1,5 +1,6 @@
import BaseAuthenticator from './base';
import { resolve, reject } from 'rsvp';
import { inject as service } from '@ember/service';
import { reject } from 'rsvp';
import fetch from 'fetch';
/**
@@ -7,56 +8,92 @@ import fetch from 'fetch';
*/
export default class OIDCAuthenticator extends BaseAuthenticator {
// =unimplemented methods
// TODO: implement ;-)
// =services
@service session;
// =methods
/**
* Generates an auth method URL with which to authenticate.
* @override
* @param {object} options
* @param {string} options.scope.scope_id
* @param {string} options.authMethod.id
* @return {string}
*/
buildAuthEndpointURL(/* {
scope: { id: scopeID },
authMethod: { id: authMethodID },
} */) { }
/**
* Generates an auth method URL with which to start authentication.
* @override
* @param {object} options
* @param {string} options.scope.scope_id
* @param {string} options.authMethod.id
* @return {string}
*/
buildStartAuthEndpointURL(/* {
scope: { id: scopeID },
authMethod: { id: authMethodID },
} */) { }
/**
* Kicks-off the OIDC flow: calls the `authenticate:start` action on the
* specified auth method, which returns some meta data along with an
* authorization request URL.
*
* @param {boolean} requestCookies request cookie tokens (default `true`)
* @param {object} options
* @return {Promise}
*/
async startAuthentication(
creds,
requestCookies = true,
options
) {
async startAuthentication(requestCookies = true, options) {
const url = this.buildStartAuthEndpointURL(options);
const body = JSON.stringify({
token_type: requestCookies ? 'cookie' : null
});
const authEndpointURL = this.buildStartAuthEndpointURL(options);
const response = await fetch(authEndpointURL, { method: 'post', body });
const response = await fetch(url, { method: 'post', body });
const json = await response.json();
const normalized = this.normalizeData(json);
if (response.status < 400) {
await this.openExternalOIDCFlow(normalized.authorization_request_url);
resolve(normalized);
// Store meta about the pending OIDC flow
this.session.set('data.pending', {
oidc: json
});
return json;
} else {
reject();
return reject();
}
}
/**
* Opens the specified URL in a new tab or window. By default this uses
* `window.open`, but may be overriden.
* @param {string} url
*
*/
async openExternalOIDCFlow(url) {
await window.open(url);
async authenticate(json) {
console.info('Authenticated with OIDC provider');
return this.normalizeData(json);
}
async authenticate(
// creds,
// requestCookies = true,
// options
) {
return false;
/**
*
*/
async attemptOIDCAuthentication(options) {
const url = this.buildAuthEndpointURL(options);
const body = JSON.stringify({
state: this.session.get('data.pending.oidc.state'),
token_request_id: this.session.get('data.pending.oidc.token_request_id')
});
const response = await fetch(url, { method: 'post', body });
const json = await response.json();
if (response.status === 100) {
return false;
} else if (response.status < 400) {
// Authentication with OIDC is indirect...
await this.session.authenticate('authenticator:oidc', json);
return true;
} else {
return reject();
}
}
}
+29 -1
View File
@@ -10,7 +10,7 @@ export default class OIDCAuthenticator extends BaseOIDCAuthenticator {
@service store;
// =attributes
// =methods
/**
* Generates an auth method URL with which to authenticate.
@@ -34,4 +34,32 @@ export default class OIDCAuthenticator extends BaseOIDCAuthenticator {
return adapter.buildURL('auth-method', authMethodID, options, 'findRecord');
}
/**
* Generates an auth method URL with which to authenticate.
* @override
* @param {object} options
* @param {string} options.scope.scope_id
* @param {string} options.authMethod.id
* @return {string}
*/
buildAuthEndpointURL({
scope: { id: scopeID },
authMethod: { id: authMethodID },
}) {
const adapter = this.store.adapterFor('application');
const options = { adapterOptions: { scopeID, method: 'authenticate' } };
return adapter.buildURL('auth-method', authMethodID, options, 'findRecord');
}
/**
* Generates an auth token validation URL used to check tokens on restoration.
* @override
* @param {string} tokenID
* @return {string}
*/
buildTokenValidationEndpointURL(tokenID) {
const adapter = this.store.adapterFor('auth-token');
return adapter.buildURL('auth-token', tokenID, {}, 'findRecord');
}
}
@@ -51,7 +51,6 @@ export default class ScopesScopeAuthenticateMethodRoute extends Route {
case 'oidc':
await this.startOIDCAuthentication(
authenticatorName,
creds,
requestCookies,
{ scope, authMethod }
);
@@ -60,8 +59,25 @@ export default class ScopesScopeAuthenticateMethodRoute extends Route {
}
}
async startOIDCAuthentication(authenticatorName, creds, requestCookies, options) {
/**
*
*/
async startOIDCAuthentication(authenticatorName, requestCookies, options) {
const oidc = getOwner(this).lookup(authenticatorName);
oidc.startAuthentication(creds, requestCookies, options);
// TODO: delegate this call from the session service so that we don't have
// to look up the authenticator directly
const json = await oidc.startAuthentication(requestCookies, options);
await this.openExternalOIDCFlow(json.authorization_request_url);
}
/**
* Opens the specified URL in a new tab or window. By default this uses
* `window.open`, but may be overriden.
* @param {string} url
*/
async openExternalOIDCFlow(url) {
// TODO don't use window directly
await window.open(url);
}
}
@@ -1,4 +1,67 @@
import Route from '@ember/routing/route';
import { getOwner } from '@ember/application';
import { inject as service } from '@ember/service';
import { task, timeout } from 'ember-concurrency';
import config from '../../../../../config/environment';
const POLL_TIMEOUT_SECONDS = config.oidcPollingTimeoutSeconds;
export default class ScopesScopeAuthenticateMethodOidcRoute extends Route {
// =services
@service session;
// =attributes
/**
* A simple Ember Concurrency-based polling task that refreshes the route
* every POLL_TIMEOUT_SECONDS seconds. This is necessary to display changes
* to session `status` that may occur.
*
* NOTE: tasks are sort of attributes and sort of methods, but they are not
* language-level constructs. Thus we annotate this task as if it
* is an attribute.
* @type {Task}
*/
@task(function * () {
while(!this.session.isAuthenticated) {
yield timeout(POLL_TIMEOUT_SECONDS * 1000);
yield this.refresh();
}
}).drop() poller;
// =methods
model() {
const scope = this.modelFor('scopes.scope');
const authMethod = this.modelFor('scopes.scope.authenticate.method');
const authenticatorName = `authenticator:${authMethod.type}`;
const oidc = getOwner(this).lookup(authenticatorName);
// TODO: delegate this call from the session service so that we don't have
// to look up the authenticator directly
return oidc.attemptOIDCAuthentication({ scope, authMethod });
}
/**
* If authenticated, redirects to index for further processing.
*/
redirect() {
if (this.session.isAuthenticated) this.replaceWith('index');
}
/**
* When this route is activated (entered), begin polling for changes.
*/
activate() {
this.poller.perform();
}
/**
* When this route is deactivated (exited), stop polling for changes.
*/
deactivate() {
this.poller.cancelAll();
}
}
@@ -1 +1 @@
OIDC yay!
Checking for OIDC ready &hellip;
+1
View File
@@ -36,6 +36,7 @@ module.exports = function (environment) {
notifyTimeout: 4000,
sessionPollingTimeoutSeconds: 2.5,
oidcPollingTimeoutSeconds: 1,
documentation: {
baseURL: 'https://boundaryproject.io/help/admin-ui',