diff --git a/config.js b/config.js
index 2c39f74b..1d93f69a 100755
--- a/config.js
+++ b/config.js
@@ -89,7 +89,9 @@ config.siop = {
pollPath: '/poll',
pollCertPath: '/cert/poll',
clientID: process.env.BAE_LP_SIOP_CLIENT_ID || 'some_id',
+ privateKey: process.env.BAE_LP_SIOP_PRIVATE_KEY,
callbackURL: process.env.BAE_LP_SIOP_CALLBACK_PATH || 'http://proxy.docker:8004/auth/vc/callback',
+ requestUri: process.env.BAE_LP_SIOP_REQUEST_URI || '/auth/vc/request.jwt',
verifierHost: process.env.BAE_LP_SIOP_VERIFIER_HOST || 'https://verifier.apps.fiware.fiware.dev',
verifierQRCodePath: process.env.BAE_LP_SIOP_VERIFIER_QRCODE_PATH || '/api/v1/loginQR',
verifierTokenPath: process.env.BAE_LP_SIOP_VERIFIER_TOKEN_PATH || '/token',
diff --git a/controllers/admin.js b/controllers/admin.js
index 5a958d40..f0ed9432 100644
--- a/controllers/admin.js
+++ b/controllers/admin.js
@@ -53,9 +53,17 @@ function admin() {
})
}
+ function isOrgAuth(user) {
+ return user.organizations.filter((org) => {
+ return org.roles.filter((role) => {
+ return role.name.toLowerCase() == 'certifier'
+ }).length > 0
+ }).length > 0
+ }
+
const uploadCertificate = async function (req, res) {
// Check user permissions
- if (!utils.isAdmin(req.user) && !utils.hasRole(req.user, 'certifier')) {
+ if (!utils.isAdmin(req.user) && !utils.hasRole(req.user, 'certifier') && !isOrgAuth(req.user)) {
res.status(403)
res.json({ error: "You are not authorized to upload certificates" })
return
diff --git a/lib/strategies/passport-vc.js b/lib/strategies/passport-vc.js
index 93e4d728..5b3cedd6 100644
--- a/lib/strategies/passport-vc.js
+++ b/lib/strategies/passport-vc.js
@@ -6,6 +6,8 @@ const fetch = require('node-fetch');
const NodeCache = require('node-cache');
const jwksClient = require('jwks-rsa');
const config = require('../../config').siop
+const EC = require('elliptic').ec;
+const crypto = require('crypto');
class CredentialSubject {
constructor(data, issuer) {
@@ -42,7 +44,7 @@ class CredentialSubject {
}
}
-class LEARCredentialSubject {
+class LEARCredSubject {
constructor(data, issuer) {
const mandate = data.mandate
const individual = mandate.mandatee
@@ -61,22 +63,53 @@ class LEARCredentialSubject {
}
const organization = mandate.mandator
- const orgRoles = mandate.power.map((power) => {
+ const orgRoles = this.mapRoles(mandate)
+
+ this.organization = {
+ id: organization.organizationIdentifier,
+ name: organization.organization,
+ roles: orgRoles
+ }
+ }
+
+ mapRoles(mandate) {
+ return []
+ }
+}
+
+class LEARCredentialSubject extends LEARCredSubject {
+ constructor(data, issuer) {
+ super(data, issuer)
+ }
+
+ mapRoles(mandate) {
+ return mandate.power.map((power) => {
let role = null
- if (power.tmf_function.toLowerCase() == 'onboarding' && power.tmf_action.toLowerCase() == 'execute') {
+ let func = ''
+ let action = ''
+
+ if (power.tmf_function != null) {
+ func = power.tmf_function.toLowerCase()
+ action = power.tmf_action
+ } else {
+ func = power.tmfFunction.toLowerCase()
+ action = power.tmfAction
+ }
+
+ if (func == 'onboarding' && action.toLowerCase() == 'execute') {
// orgAdmin
role = {
'id': 'orgAdmin',
'name': 'orgAdmin'
}
- } else if (power.tmf_function.toLowerCase() == 'productoffering'
- && power.tmf_action.includes('Create') && power.tmf_action.includes('Update')) {
+ } else if (func == 'productoffering'
+ && action.includes('Create') && action.includes('Update')) {
// Seller
role = {
'id': 'seller',
'name': 'seller'
}
- } else if (power.tmf_function.toLowerCase() == 'certification' && power.tmf_action.includes('UploadCertificate')) {
+ } else if (func == 'certification' && (action.includes('UploadCertificate') || action.includes('post_verifiable_certification'))) {
role = {
'id': 'certifier',
'name': 'certifier'
@@ -88,12 +121,59 @@ class LEARCredentialSubject {
}).filter((role) => {
return role != null
})
+ }
+}
- this.organization = {
- id: organization.organizationIdentifier,
- name: organization.organization,
- roles: orgRoles
+class LEARCredentialMachineSubject extends LEARCredSubject {
+ constructor(data, issuer) {
+ super(data, issuer)
+
+ const mandate = data.mandate
+ const individual = mandate.mandatee
+ const org = mandate.mandator
+
+ if (!this.email) {
+ if (individual.contact && individual.contact.email) {
+ this.email = individual.contact.email
+ } else {
+ this.email = org.emailAddress
+ }
}
+
+ if (!this.familyName) {
+ this.familyName = individual.serviceName
+ this.firstName = individual.serviceName
+ }
+ }
+
+ mapRoles(mandate) {
+ return mandate.power.map((power) => {
+ let role = null
+
+ if (power.function.toLowerCase() == 'onboarding' && power.action.toLowerCase() == 'execute') {
+ // orgAdmin
+ role = {
+ 'id': 'orgAdmin',
+ 'name': 'orgAdmin'
+ }
+ } else if (power.function.toLowerCase() == 'productoffering'
+ && power.action.includes('Create') && power.action.includes('Update')) {
+ // Seller
+ role = {
+ 'id': 'seller',
+ 'name': 'seller'
+ }
+ } else if (power.function.toLowerCase() == 'certification' && (power.action.includes('UploadCertificate') || power.action.includes('post_verifiable_certification'))) {
+ role = {
+ 'id': 'certifier',
+ 'name': 'certifier'
+ }
+ }
+
+ return role
+ }).filter((role) => {
+ return role != null
+ })
}
}
@@ -101,7 +181,8 @@ class VerifiableCredential {
constructor(payload) {
const supportedCredentials = {
LEARCredentialEmployee: LEARCredentialSubject,
- LegalPersonCredential: CredentialSubject
+ LegalPersonCredential: CredentialSubject,
+ LEARCredentialMachine: LEARCredentialMachineSubject
};
const processSubject = (data) => {
@@ -168,32 +249,20 @@ class VCStrategy extends Strategy {
this.jwksClient = jwksClient({
jwksUri: options.verifierJWKSURL
});
+
this.allowedRoles = options.allowedRoles;
this.verifierTokenURL = options.verifierTokenURL;
+ this.verifierHost = options.verifierHost;
this.redirectURI = options.redirectURI;
this.isRedirection = options.isRedirection;
+ this.clientID = options.clientID;
+ this.privateKey = options.privateKey;
this._verify = verify;
this._stateCache = new NodeCache();
}
- requestToken(req, authCode) {
- const params = {
- 'code': authCode,
- 'grant_type': 'authorization_code',
- 'redirect_uri': this.redirectURI
- };
-
- if (req.query != null && req.query.callback_url != null) {
- let oldUrl = new URL(this.redirectURI)
- let newUrl = new URL(req.query.callback_url)
-
- newUrl.pathname = oldUrl.pathname
-
- params.redirect_uri = newUrl.toString()
- }
-
- console.log(params)
+ makeTokenRequest(params) {
const reqParams = new URLSearchParams(params);
fetch(this.verifierTokenURL, {
@@ -215,6 +284,78 @@ class VCStrategy extends Strategy {
});
}
+ requestTokenWithClientSign(req, authCode) {
+ const tokenInfo = {
+ 'iss': this.clientID,
+ 'aud': this.verifierHost,
+ 'sub': this.clientID,
+ 'exp': Math.floor(Date.now() / 1000) + 30
+ }
+
+ try {
+ console.log('Building eliptic')
+
+ const ec = new EC('p256'); // P-256 curve (also known as secp256r1)
+ const key = ec.keyFromPrivate(this.privateKey);
+
+ // Get the public key in uncompressed format (includes both x and y coordinates)
+ const publicKey = key.getPublic();
+
+ console.log('Building public key')
+ // Extract x and y coordinates and encode them in Base64url format
+ const x = publicKey.getX().toString('hex'); // Hex representation of x
+ const y = publicKey.getY().toString('hex'); // Hex representation of y
+
+ const jwk = {
+ kty: 'EC',
+ crv: 'P-256',
+ d: Buffer.from(this.privateKey, 'hex').toString('base64url'),
+ x: Buffer.from(x, 'hex').toString('base64url'),
+ y: Buffer.from(y, 'hex').toString('base64url')
+ }
+
+ const keyObject = crypto.createPrivateKey({ format: 'jwk', key: jwk })
+
+ console.log('Signing token')
+ const token = jwt.sign(tokenInfo, keyObject, {
+ keyid: this.clientID,
+ algorithm: 'ES256'
+ })
+
+ const params = {
+ 'grant_type': 'authorization_code',
+ 'code': authCode,
+ 'client_id': this.clientID,
+ 'client_assertion_type': 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer',
+ 'client_assertion': token
+ }
+
+ console.log('Calling token')
+ this.makeTokenRequest(params)
+ } catch (e) {
+ console.log(e)
+ }
+ }
+
+ requestToken(req, authCode) {
+ const params = {
+ 'code': authCode,
+ 'grant_type': 'authorization_code',
+ 'redirect_uri': this.redirectURI
+ };
+
+ if (req.query != null && req.query.callback_url != null) {
+ let oldUrl = new URL(this.redirectURI)
+ let newUrl = new URL(req.query.callback_url)
+
+ newUrl.pathname = oldUrl.pathname
+
+ params.redirect_uri = newUrl.toString()
+ }
+
+ this.makeTokenRequest(params)
+ }
+
authenticatePolling(req, options) {
if (req.query && req.query.state && req.query.code) {
console.log("________ AUTH CODE ______________")
@@ -249,7 +390,7 @@ class VCStrategy extends Strategy {
console.log("Code: " + req.query.code)
const authCode = req.query.code
- this.requestToken(req, authCode);
+ this.requestTokenWithClientSign(req, authCode);
}
authenticate(req, options) {
@@ -310,34 +451,38 @@ class VCStrategy extends Strategy {
verifyToken(accessToken, callback) {
console.log("______ TOKEN VERIFICATION ___________")
- const payload = jwt.decode(accessToken);
+ const decodedToken = jwt.decode(accessToken, {'complete': true});
+ const header = decodedToken.header;
+ const payload = decodedToken.payload;
+
+ // Get kid
+ let kid = null
+ if (header && header['kid']) {
+ kid = header['kid']
+ } else if (payload && payload['kid']) {
+ kid = payload['kid']
+ } else {
+ return callback(new Error('Access token has wrong format'));
+ }
// return callback(null, payload)
// console.log(accessToken)
// console.log('_______________________')
- if (payload && payload['kid']) {
- this.jwksClient.getSigningKey(payload['kid'], (err, signingKey) => {
+ this.jwksClient.getSigningKey(kid, (err, signingKey) => {
+ if (err) {
+ callback(err);
+ }
+ const publicKey = signingKey.getPublicKey();
+ jwt.verify(accessToken, publicKey, (err, decoded) => {
if (err) {
callback(err);
}
- const publicKey = signingKey.getPublicKey();
- jwt.verify(accessToken, publicKey, (err, decoded) => {
- if (err) {
- callback(err);
- }
- console.log("______ TOKEN DECODED _______")
- console.log(decoded)
- console.log('_______________________')
-
- callback(null, decoded);
- });
+ callback(null, decoded);
});
- } else {
- callback(new Error('Access token has wrong format'));
- }
+ });
}
}
diff --git a/lib/strategies/vc.js b/lib/strategies/vc.js
index 6cd2142f..731189c1 100644
--- a/lib/strategies/vc.js
+++ b/lib/strategies/vc.js
@@ -1,18 +1,25 @@
const VCStrategy = require('./passport-vc').Strategy;
+const jwt = require('jsonwebtoken');
+const EC = require('elliptic').ec;
+const crypto = require('crypto');
function strategy(config) {
function buildStrategy(callback) {
const params = {
+ verifierHost: config.verifierHost,
verifierTokenURL: config.verifierHost + config.verifierTokenPath,
verifierJWKSURL: config.verifierHost + config.verifierJWKSPath,
redirectURI: config.callbackURL,
allowedRoles: config.allowedRoles,
- isRedirection: config.isRedirection
+ isRedirection: config.isRedirection,
+ clientID: config.clientID,
+ privateKey: config.privateKey
};
return new VCStrategy(params, (accessToken, refreshToken, profile, done) => {
+ console.log('======= _verify method ==========')
callback(accessToken, refreshToken, profile, done);
});
}
@@ -27,4 +34,43 @@ function strategy(config) {
}
}
-exports.strategy = strategy;
\ No newline at end of file
+function buildRequestJWT(config) {
+ console.log(config)
+
+ const tokenInfo = {
+ "iss": config.clientID,
+ "aud": config.verifierHost,
+ "response_type": "code",
+ "client_id": config.clientID,
+ "redirect_uri": config.callbackURL,
+ "scope": "openid learcredential",
+ }
+
+ const ec = new EC('p256'); // P-256 curve (also known as secp256r1)
+ const key = ec.keyFromPrivate(config.privateKey);
+
+ // Get the public key in uncompressed format (includes both x and y coordinates)
+ const publicKey = key.getPublic();
+
+ // Extract x and y coordinates and encode them in Base64url format
+ const x = publicKey.getX().toString('hex'); // Hex representation of x
+ const y = publicKey.getY().toString('hex'); // Hex representation of y
+
+ const jwk = {
+ kty: 'EC',
+ crv: 'P-256',
+ d: Buffer.from(config.privateKey, 'hex').toString('base64url'),
+ x: Buffer.from(x, 'hex').toString('base64url'),
+ y: Buffer.from(y, 'hex').toString('base64url')
+ }
+
+ const keyObject = crypto.createPrivateKey({ format: 'jwk', key: jwk })
+
+ return jwt.sign(tokenInfo, keyObject, {
+ keyid: config.clientID,
+ algorithm: 'ES256'
+ });
+}
+
+exports.strategy = strategy;
+exports.buildRequestJWT = buildRequestJWT;
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index 1fdf1025..c4190b07 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "business-ecosystem-logic-proxy",
- "version": "9.0.0",
+ "version": "9.8.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "business-ecosystem-logic-proxy",
- "version": "9.0.0",
+ "version": "9.8.0",
"license": "AGPL-3.0",
"dependencies": {
"async": "^1.5.0",
@@ -18,6 +18,7 @@
"cookie-parser": "1.4.0",
"deep-equal": "^1.0.1",
"deepcopy": "^0.6.3",
+ "elliptic": "^6.5.7",
"errorhandler": "1.4.x",
"express": "^4.x",
"express-session": "^1.17.3",
@@ -858,6 +859,11 @@
"resolved": "https://registry.npmjs.org/blueimp-md5/-/blueimp-md5-2.19.0.tgz",
"integrity": "sha512-DRQrD6gJyy8FbiE4s+bDoXS9hiW3Vbx5uCdwvcCf3zLHL+Iv7LtGHLpr+GZV8rHG8tK766FGYBwRbu8pELTt+w=="
},
+ "node_modules/bn.js": {
+ "version": "4.12.0",
+ "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz",
+ "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA=="
+ },
"node_modules/body-parser": {
"version": "1.20.2",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz",
@@ -981,10 +987,15 @@
"concat-map": "0.0.1"
}
},
+ "node_modules/brorand": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz",
+ "integrity": "sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w=="
+ },
"node_modules/bson": {
- "version": "6.1.0",
- "resolved": "https://registry.npmjs.org/bson/-/bson-6.1.0.tgz",
- "integrity": "sha512-yiQ3KxvpVoRpx1oD1uPz4Jit9tAVTJgjdmjDKtUErkOoL9VNoF8Dd58qtAOL5E40exx2jvAT9sqdRSK/r+SHlA==",
+ "version": "6.8.0",
+ "resolved": "https://registry.npmjs.org/bson/-/bson-6.8.0.tgz",
+ "integrity": "sha512-iOJg8pr7wq2tg/zSlCCHMi3hMm5JTOxLTagf3zxhcenHsFp+c6uOs6K7W5UE7A4QIJGtqh/ZovFNMP4mOPJynQ==",
"engines": {
"node": ">=16.20.1"
}
@@ -1806,6 +1817,20 @@
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="
},
+ "node_modules/elliptic": {
+ "version": "6.5.7",
+ "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.7.tgz",
+ "integrity": "sha512-ESVCtTwiA+XhY3wyh24QqRGBoP3rEdDUl3EDUUo9tft074fi19IrdpH7hLCMMP3CIj7jb3W96rn8lt/BqIlt5Q==",
+ "dependencies": {
+ "bn.js": "^4.11.9",
+ "brorand": "^1.1.0",
+ "hash.js": "^1.0.0",
+ "hmac-drbg": "^1.0.1",
+ "inherits": "^2.0.4",
+ "minimalistic-assert": "^1.0.1",
+ "minimalistic-crypto-utils": "^1.0.1"
+ }
+ },
"node_modules/encodeurl": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz",
@@ -2510,6 +2535,15 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/hash.js": {
+ "version": "1.1.7",
+ "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz",
+ "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==",
+ "dependencies": {
+ "inherits": "^2.0.3",
+ "minimalistic-assert": "^1.0.1"
+ }
+ },
"node_modules/he": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/he/-/he-1.1.1.tgz",
@@ -2518,6 +2552,16 @@
"he": "bin/he"
}
},
+ "node_modules/hmac-drbg": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz",
+ "integrity": "sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==",
+ "dependencies": {
+ "hash.js": "^1.0.3",
+ "minimalistic-assert": "^1.0.0",
+ "minimalistic-crypto-utils": "^1.0.1"
+ }
+ },
"node_modules/home-or-tmp": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/home-or-tmp/-/home-or-tmp-2.0.0.tgz",
@@ -3726,6 +3770,16 @@
"node": ">=4"
}
},
+ "node_modules/minimalistic-assert": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
+ "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A=="
+ },
+ "node_modules/minimalistic-crypto-utils": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz",
+ "integrity": "sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg=="
+ },
"node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
diff --git a/package.json b/package.json
index 789573b3..d2a73635 100755
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "business-ecosystem-logic-proxy",
- "version": "9.0.0",
+ "version": "9.8.0",
"description": "Logic Layer for the Business API Ecosystem",
"author": "FICODES",
"license": "AGPL-3.0",
@@ -17,6 +17,7 @@
"cookie-parser": "1.4.0",
"deep-equal": "^1.0.1",
"deepcopy": "^0.6.3",
+ "elliptic": "^6.5.7",
"errorhandler": "1.4.x",
"express": "^4.x",
"express-session": "^1.17.3",
diff --git a/portal/bae-frontend/index.html b/portal/bae-frontend/index.html
index c2081e0a..41196689 100644
--- a/portal/bae-frontend/index.html
+++ b/portal/bae-frontend/index.html
@@ -6,7 +6,7 @@
-
+
-->
-