Skip to content

Commit

Permalink
Merge pull request #58 from 5minds/feature/fix-credentials-update-cycle
Browse files Browse the repository at this point in the history
Fügt einen refresh cycle für ProcessCube Config Node hinzu
  • Loading branch information
luisthieme authored Nov 15, 2024
2 parents 21476e8 + 2b32369 commit a8e9c4b
Show file tree
Hide file tree
Showing 3 changed files with 88 additions and 115 deletions.
12 changes: 6 additions & 6 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@
"examples": "examples"
},
"dependencies": {
"@5minds/processcube_engine_client": "^5.1.0",
"@5minds/processcube_engine_client": "5.1.0-hotfix-a73235-m346kg1n",
"jwt-decode": "^4.0.0",
"openid-client": "^5.5.0"
},
Expand Down
189 changes: 81 additions & 108 deletions processcube-engine-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,20 @@ const engine_client = require('@5minds/processcube_engine_client');
const jwt = require('jwt-decode');
const oidc = require('openid-client');

const DELAY_FACTOR = 0.85;

module.exports = function (RED) {
function ProcessCubeEngineNode(n) {
RED.nodes.createNode(this, n);
const node = this;
const identityChangedCallbacks = [];
this.url = RED.util.evaluateNodeProperty(n.url, n.urlType, node);
this.identity = null;

this.credentials.clientId = RED.util.evaluateNodeProperty(n.clientId, n.clientIdType, node);
this.credentials.clientSecret = RED.util.evaluateNodeProperty(n.clientSecret, n.clientSecretType, node);

// known issue: kann bei falschem timing zu laufzeitfehlern führen (absprache MM)
// set the engine url
const stopRefreshing = periodicallyRefreshEngineClient(this, n, 10000);

this.registerOnIdentityChanged = function (callback) {
identityChangedCallbacks.push(callback);
};
Expand All @@ -36,123 +37,95 @@ module.exports = function (RED) {
}
};

node.on('close', async () => {
if (this.engineClient) {
this.engineClient.dispose();
this.engineClient = null;
function periodicallyRefreshEngineClient(node, n, intervalMs) {
function refreshUrl() {
const newUrl = RED.util.evaluateNodeProperty(n.url, n.urlType, node);

if (node.url == newUrl) {
return;
}

node.url = newUrl;
if (node.credentials.clientId && node.credentials.clientSecret) {
if (this.engineClient) {
this.engineClient.dispose();
}
node.engineClient = new engine_client.EngineClient(node.url, () =>
getFreshIdentity(node.url, node)
);
} else {
if (this.engineClient) {
this.engineClient.dispose();
}
node.engineClient = new engine_client.EngineClient(node.url);
}
}
});

if (this.credentials.clientId && this.credentials.clientSecret) {
this.engineClient = new engine_client.EngineClient(this.url);

this.engineClient.applicationInfo
.getAuthorityAddress()
.then((authorityUrl) => {
startRefreshingIdentityCycle(
this.credentials.clientId,
this.credentials.clientSecret,
authorityUrl,
node
).catch((reason) => {
console.error(reason);
node.error(reason);
});
})
.catch((reason) => {
console.error(reason);
node.error(reason);
});
} else {
this.engineClient = new engine_client.EngineClient(this.url);
refreshUrl();
const intervalId = setInterval(refreshUrl, intervalMs);

return () => clearInterval(intervalId);
}
}
RED.nodes.registerType('processcube-engine-config', ProcessCubeEngineNode, {
credentials: {
clientId: { type: 'text' },
clientSecret: { type: 'password' },
},
});
};

async function getFreshTokenSet(clientId, clientSecret, authorityUrl) {
const issuer = await oidc.Issuer.discover(authorityUrl);
async function getFreshIdentity(url, node) {
try {
if (
!RED.util.evaluateNodeProperty(n.clientId, n.clientIdType, node) ||
!RED.util.evaluateNodeProperty(n.clientSecret, n.clientSecretType, node)
) {
return null;
}

const res = await fetch(url + '/atlas_engine/api/v1/authority', {
method: 'GET',
headers: {
Authorization: `Bearer ZHVtbXlfdG9rZW4=`,
'Content-Type': 'application/json',
},
});

const client = new issuer.Client({
client_id: clientId,
client_secret: clientSecret,
});
const body = await res.json();

const tokenSet = await client.grant({
grant_type: 'client_credentials',
scope: 'engine_etw engine_read engine_write',
});
const issuer = await oidc.Issuer.discover(body);

return tokenSet;
}
const client = new issuer.Client({
client_id: RED.util.evaluateNodeProperty(n.clientId, n.clientIdType, node),
client_secret: RED.util.evaluateNodeProperty(n.clientSecret, n.clientSecretType, node),
});

function getIdentityForExternalTaskWorkers(tokenSet) {
const accessToken = tokenSet.access_token;
const decodedToken = jwt.jwtDecode(accessToken);
const tokenSet = await client.grant({
grant_type: 'client_credentials',
scope: 'engine_etw engine_read engine_write',
});

return {
token: tokenSet.access_token,
userId: decodedToken.sub,
};
}
const accessToken = tokenSet.access_token;
const decodedToken = jwt.jwtDecode(accessToken);

async function getExpiresInForExternalTaskWorkers(tokenSet) {
let expiresIn = tokenSet.expires_in;
const freshIdentity = {
token: tokenSet.access_token,
userId: decodedToken.sub,
};

if (!expiresIn && tokenSet.expires_at) {
expiresIn = Math.floor(tokenSet.expires_at - Date.now() / 1000);
}

if (expiresIn === undefined) {
throw new Error('Could not determine the time until the access token for external task workers expires');
}
node.setIdentity(freshIdentity);

return expiresIn;
}

/**
* Start refreshing the identity in regular intervals.
* @param {TokenSet | null} tokenSet The token set to refresh the identity for
* @returns {Promise<void>} A promise that resolves when the timer for refreshing the identity is initialized
* */
async function startRefreshingIdentityCycle(clientId, clientSecret, authorityUrl, configNode) {
let retries = 5;

const refresh = async () => {
try {
const newTokenSet = await getFreshTokenSet(clientId, clientSecret, authorityUrl);
const expiresIn = await getExpiresInForExternalTaskWorkers(newTokenSet);
const delay = expiresIn * DELAY_FACTOR * 1000;

freshIdentity = getIdentityForExternalTaskWorkers(newTokenSet);

configNode.setIdentity(freshIdentity);

retries = 5;
setTimeout(refresh, delay);
} catch (error) {
if (retries === 0) {
console.error(
'Could not refresh identity for external task worker processes. Stopping all external task workers.',
{ error }
);
return;
return freshIdentity;
} catch (e) {
node.error(`Could not get fresh identity: ${e}`);
}
console.error('Could not refresh identity for external task worker processes.', {
error,
retryCount: retries,
});
retries--;

const delay = 2 * 1000;
setTimeout(refresh, delay);
}
};

await refresh();
}
node.on('close', async () => {
if (this.engineClient) {
stopRefreshing();
this.engineClient.dispose();
this.engineClient = null;
}
});
}
RED.nodes.registerType('processcube-engine-config', ProcessCubeEngineNode, {
credentials: {
clientId: { type: 'text' },
clientSecret: { type: 'password' },
},
});
};

0 comments on commit a8e9c4b

Please sign in to comment.