/**
*
* @class
* @classdesc
* Provides methods to connect to and manage the IDP.
* It assumes that the user has already been authenticated by the IDP.
* This will typically have been done by the Apache OIDC module on the server before the page is served.
*
* ## Dependencies ##
* * The Keycloak JavaScript adapter must have been loaded from the IDP via a script tag:
* ```html
* <script src="https://idp.leadershipfactor.com/auth/js/keycloak.js"></script>
* ```
*
* * A page must exist at <code><em>site_root</em>/status/silent-check-sso.html</code> with the following code:
* ```html
* <html>
* <body>
* <script>
* parent.postMessage(location.href, location.origin);
* </script>
* </body>
* </html>
* ```
* See {@link https://github.com/keycloak/keycloak-documentation/blob/master/securing_apps/topics/oidc/javascript-adapter.adoc Keycloak JavaScript Adapter} for details.
*
* @copyright (c) 2021 TLF Research Ltd.
*
* @example
* const idp = new IDP("https://idp.leadershipfactor.com", "Company X", "portal1");
*
* @param {string} root Root URL for the IDP that manages this client
* @param {string} realm The name of the IDP realm that controls this client
* @param {string} client The name of this "client" i.e. the application or site name as defined on the IDP
*
*/
function IDP(root, realm, client) {
const self = this;
self.root = root;
self.realm = realm;
// Public methods
self.init = init.bind(self);
self.get = get.bind(self);
self.post = post.bind(self);
self.del = del.bind(self);
self.put = put.bind(self);
const keycloak = new Keycloak({
'realm': realm,
'auth-server-url': root + '/auth/',
'ssl-required': 'external',
'public-client': true,
'confidential-port': 0,
'url': root + '/auth',
'clientId': client,
'enable-cors': true
});
function init() {
const self = this;
return new Promise(function (resolve, reject) {
keycloak.init({
promiseType: 'native',
onLoad: 'check-sso',
enableLogging: true,
silentCheckSsoRedirectUri: window.location.origin + '/status/silent-check-sso.html'
}).then(function () {
if (keycloak.token) {
document.body.addEventListener('beforeLogout', function () { keycloak.logout() });
resolve();
} else {
keycloak.login();
resolve(Error('Not authenticated'));
}
}).catch(function (err) {
reject(err);
});
});
}
/**
* Fetches a resource asynchronously from the IDP via an HTTP GET method.
* This is an idempotent method.
*
* @example
* idp.get("roles")
* .then(roles => {
* // Do something with roles here
* }
* .catch(err => {
* // Something went wrong
* console.log(err);
* });
*
* @param {string} url URL of the endpoint to invoke on the IDP. Omit the root, including its traling slash
*
* @return {Promise}
*/
function get(url) {
return exec.call(this, 'GET', url);
}
/**
* Creates a new resource asynchronously on the IDP via an HTTP POST method.
*
* @example
* idp.post(`users/${userid}/role-mappings/realm`, [newRole]).catch(err => console.log(err));
*
* @param {string} url URL of the endpoint to invoke on the IDP. Omit the root, including its traling slash
* @param {object} data Data to apply as the update
*
* @return {Promise}
*/
function post(url, data) {
return exec.call(this, 'POST', url, data);
}
/**
* Deletes a resource asynchronously from the IDP via an HTTP DELETE method.
*
* @example
* idp.del(`users/${userid}/role-mappings/realm`, [oldRole]).catch(err => console.log(err));
* @param {string} url URL of the endpoint to invoke on the IDP. Omit the root, including its traling slash
* @param {object} data Data identifying the resource to be removed
*
* @return {Promise}
*/
function del(url, data) {
return exec.call(this, 'DELETE', url, data);
}
/**
* Updates a resource asynchronously on the IDP via an HTTP PUT method.
* This is an idempotent method.
*
* @example
* idp.put(`users/${userid}`, { enabled: true }).catch(err => console.log(err))
*
* @param {string} url URL of the endpoint to invoke on the IDP. Omit the root, including its traling slash
* @param {object} data JSON-encoded data to apply as the update
*
* @return {Promise}
*/
function put(url, data) {
return exec.call(this, 'PUT', url, data);
}
/**
* Executes the given remote call via Ajax.
*
* @private
*
* @param {string} method HTTP operation to perform - one of 'GET', 'PUT', 'DELETE' or 'POST'
* @param {string} url URL of the endpoint to invoke on the IDP. Omit the root, including its traling slash
* @param {object} [data] Data to be sent
*
* @return {Promise}
*/
function exec(method, url, data) {
const self = this;
return new Promise(function (resolve, reject) {
// Need to update our access token first before trying to fetch any data
keycloak.updateToken(30)
.then(function () {
if (!keycloak.token) {
reject(Error('Missing access token'));
return;
}
fetch(self.root + '/auth/admin/realms/' + self.realm + '/' + url,
{
method: method,
headers: {
'Accept': 'application/json',
'Authorization': 'Bearer ' + keycloak.token,
'Content-Type': 'application/json;charset=utf-8'
},
body: JSON.stringify(data)
})
.then(function (resp) {
if (!resp.ok) {
throw new Error(method + ' to the IDP at ' + url + ' failed (' + resp.status + ')');
}
switch (resp.status) {
case 200:
return resp.json();
}
})
.then(function (data) { resolve(data) })
.catch(function (err) { reject(err) })
})
.catch(function (err) {
reject(Error('Failed to refresh IDP access token: ' + err));
});
});
}
}