Skip to content

Commit

Permalink
Update SIOP support to be OIDC compliant (#299)
Browse files Browse the repository at this point in the history
* Update SIOP auth to be OIDC compliant
* Update VC with redirection tests
* Avoid the need of having to configure the whole request JWT endpoint
* Use ES256 for signing request tokens
* Fix issues accessing kid
* Add support for cammelcase VC power frmat
* Add Inventory section and organization profile view to portal
* Add default values for navigation links
* Add support to LEARCredentialMachine VC
* Fix issues when using VC token as an Authorization header
* Update certification powers to support post_verifiable_certification
* Add tests to validate certificate uploads
* Add a test to validate LEARCredentialMachine VCs
* Set pipeline and package version to new release
  • Loading branch information
fdelavega authored Oct 22, 2024
1 parent 76784db commit d12e06a
Show file tree
Hide file tree
Showing 12 changed files with 735 additions and 151 deletions.
2 changes: 2 additions & 0 deletions config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
10 changes: 9 additions & 1 deletion controllers/admin.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
237 changes: 191 additions & 46 deletions lib/strategies/passport-vc.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -42,7 +44,7 @@ class CredentialSubject {
}
}

class LEARCredentialSubject {
class LEARCredSubject {
constructor(data, issuer) {
const mandate = data.mandate
const individual = mandate.mandatee
Expand All @@ -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'
Expand All @@ -88,20 +121,68 @@ 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
})
}
}

class VerifiableCredential {
constructor(payload) {
const supportedCredentials = {
LEARCredentialEmployee: LEARCredentialSubject,
LegalPersonCredential: CredentialSubject
LegalPersonCredential: CredentialSubject,
LEARCredentialMachine: LEARCredentialMachineSubject
};

const processSubject = (data) => {
Expand Down Expand Up @@ -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, {
Expand All @@ -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 ______________")
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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'));
}
});
}
}

Expand Down
Loading

0 comments on commit d12e06a

Please sign in to comment.