Skip to content

Commit

Permalink
additional changes related to ucl-mtls
Browse files Browse the repository at this point in the history
  • Loading branch information
aramovic79 committed Jan 29, 2025
1 parent c550441 commit 863f155
Show file tree
Hide file tree
Showing 7 changed files with 123 additions and 65 deletions.
5 changes: 4 additions & 1 deletion __tests__/unittest/authentication.test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
const cds = require('@sap/cds');
const { AUTHENTICATION_TYPE } = require('../../lib/constants');
const authenticate = require('../../lib/authentication');
const { authenticate }= require('../../lib/authentication');

describe('Authentication Middleware', () => {
const mockValidUser = { admin: "secret" };
Expand Down Expand Up @@ -131,6 +131,9 @@ describe('Authentication Middleware', () => {

describe("UCL mTLS Authentication", () => {
beforeEach(async () => {
// mock cds.context
// cds.context = {
// tru
// server = fastify();
// server.setErrorHandler(errorHandler);
// await setupAuthentication(server, {
Expand Down
104 changes: 73 additions & 31 deletions lib/authentication.js
Original file line number Diff line number Diff line change
@@ -1,41 +1,62 @@
const cds = require("@sap/cds");
const { AUTHENTICATION_TYPE } = require("./constants");
const { AUTHENTICATION_TYPE, CERT_SUBJECT_HEADER_KEY } = require("./constants");
const { Logger } = require("./logger");

/**
* Depending on authentication type, return the credentials in case of the basic authentication
* or mtls endpoints in case of UCL-mtls
*/
function getAuthenticationDetails() {
function getBasicAuthDetails() {
const basicAuthData = process.env.APP_USERS ?
JSON.parse(process.env.APP_USERS) :
cds.env.authentication.credentials ?
JSON.parse(cds.env.authentication.credentials) : {};

return Object.entries(basicAuthData).map(([username, password]) => ({ username, password }))[0];
}
function getAuthenticationType() {
return process.env.ORD_AUTH || cds.env.authentication?.type || AUTHENTICATION_TYPE.Open;
}

switch (process.env.ORD_AUTH || cds.env.authentication?.type || AUTHENTICATION_TYPE.Open) {
case AUTHENTICATION_TYPE.Basic:
function getAuthData(type) {
switch (type) {
case AUTHENTICATION_TYPE.Basic: {
const basicAuthData = process.env.APP_USERS ?
JSON.parse(process.env.APP_USERS) :
cds.env.authentication.credentials ?
JSON.parse(cds.env.authentication.credentials) : {};
return {
type: AUTHENTICATION_TYPE.Basic,
...getBasicAuthDetails()
};
case AUTHENTICATION_TYPE.UclMtls:
type,
...Object.entries(basicAuthData).map(([username, password]) => ({ username, password }))[0]
}
}
case AUTHENTICATION_TYPE.UclMtls: {
const infoEndpoints = process.env.UCL_MTLS_ENDPOINTS ?
JSON.parse(process.env.UCL_MTLS_ENDPOINTS) :
cds.env.authentication.uclMtlsEndpoints ?
cds.env.authentication.uclMtlsEndpoints : [];
return {
type: AUTHENTICATION_TYPE.UclMtls,
infoEndpoints: process.env.UCL_MTLS_ENDPOINTS ?? cds.env.authentication.uclMtlsEndpoints
type,
infoEndpoints
};
}
default:
return {
type: AUTHENTICATION_TYPE.Open
};
return {};
}
}

async function fetchTrustedSubject(endpoint) {
try {
console.log('TrustedSubjectService:', endpoint);
return null;
// const resp = await fetch(endpoint);
// const jsonBody = (await resp.json());
// return jsonBody.certSubject;
} catch (error) {
Logger.error('TrustedSubjectService:', error.message);
return null;
}
}

async function getTrustedSubjects() {
const authData = getAuthData(AUTHENTICATION_TYPE.UclMtls);
const subjects = await Promise.all(authData.infoEndpoints.map((endpoint) => fetchTrustedSubject(endpoint)));
return subjects.filter((subject) => subject !== null);
}

// Middleware for authentication
module.exports = (req, res, next) => {
const authData = getAuthenticationDetails();
async function authenticate(req, res, next) {
const authType = getAuthenticationType();
const authData = getAuthData(authType);

switch (authData.type) {
case AUTHENTICATION_TYPE.Basic: {
const authHeader = req.headers['authorization'];
Expand All @@ -57,15 +78,36 @@ module.exports = (req, res, next) => {
}
break;
}
case AUTHENTICATION_TYPE.UclMtls:
case AUTHENTICATION_TYPE.UclMtls: {
const certSubjectEncoded = req.headers[CERT_SUBJECT_HEADER_KEY] || "";
const certSubject = Buffer.from(certSubjectEncoded, "base64").toString("ascii");
const certSubjectTokens = certSubject.split(",").filter((token) => token);

const matchesSomeTrustedSubject = cds.context.trustedSubjects
.map((subj) => subj.split(",").map((token) => token.trim()))
.some(
(trustedIssuerTokens) =>
trustedIssuerTokens.length === certSubjectTokens.length &&
trustedIssuerTokens.every((token) => certSubjectTokens.includes(token)),
);

if (!certSubject || !matchesSomeTrustedSubject) {
return res.status(401).send('Certificate subject header is missing or invalid');
}

res.status(200);
next();
break;
// const infoEndpoints = cds.env.infoEndpoints
// return uclMtlsAuthentication(req, res, next);
}
default: // Open access
res.status(200);
next();
break;
}
}
}

module.exports = {
authenticate,
getAuthenticationType,
getTrustedSubjects,
};
49 changes: 26 additions & 23 deletions lib/constants.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,21 @@
const AUTHENTICATION_TYPE = Object.freeze({
Open: "open",
Basic: "basic",
UclMtls: "ucl-mtls",
});

const ORD_ACCESS_STRATEGY = Object.freeze({
Open: "open",
Basic: "sap.businesshub:basic-auth:v1",
UclMtls: "sap:cmp-mtls:v1",
});

const AUTH_TYPE_ORD_ACCESS_STRATEGY_MAP = Object.freeze({
[AUTHENTICATION_TYPE.Open]: ORD_ACCESS_STRATEGY.Open,
[AUTHENTICATION_TYPE.Basic]: ORD_ACCESS_STRATEGY.Basic,
[AUTHENTICATION_TYPE.UclMtls]: ORD_ACCESS_STRATEGY.UclMtls,
});

const CDS_ELEMENT_KIND = Object.freeze({
action: "action",
annotation: "annotation",
Expand All @@ -9,6 +27,8 @@ const CDS_ELEMENT_KIND = Object.freeze({
type: "type",
});

const CERT_SUBJECT_HEADER_KEY = "x-ssl-client-subject-dn";

const COMPILER_TYPES = Object.freeze({
oas3: "oas3",
asyncapi2: "asyncapi2",
Expand Down Expand Up @@ -46,43 +66,26 @@ const RESOURCE_VISIBILITY = Object.freeze({
private: "private",
});

const SEM_VERSION_REGEX = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/;

const SHORT_DESCRIPTION_PREFIX = "Short description of ";

const AUTHENTICATION_TYPE = Object.freeze({
Open: "open",
Basic: "basic",
UclMtls: "ucl-mtls",
});

const ORD_ACCESS_STRATEGY = Object.freeze({
Open: "open",
Basic: "sap.businesshub:basic-auth:v1",
UclMtls: "sap:cmp-mtls:v1",
});

const AUTH_TYPE_ORD_ACCESS_STRATEGY_MAP = Object.freeze({
[AUTHENTICATION_TYPE.Open]: ORD_ACCESS_STRATEGY.Open,
[AUTHENTICATION_TYPE.Basic]: ORD_ACCESS_STRATEGY.Basic,
[AUTHENTICATION_TYPE.UclMtls]: ORD_ACCESS_STRATEGY.UclMtls,
});
const SEM_VERSION_REGEX = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/;

module.exports = {
AUTHENTICATION_TYPE,
AUTH_TYPE_ORD_ACCESS_STRATEGY_MAP,
CDS_ELEMENT_KIND,
CERT_SUBJECT_HEADER_KEY,
COMPILER_TYPES,
CONTENT_MERGE_KEY,
DESCRIPTION_PREFIX,
ENTITY_RELATIONSHIP_ANNOTATION,
LEVEL,
OPEN_RESOURCE_DISCOVERY_VERSION,
ORD_ACCESS_STRATEGY,
ORD_EXTENSIONS_PREFIX,
ORD_ODM_ENTITY_NAME_ANNOTATION,
ORD_RESOURCE_TYPE,
RESOURCE_VISIBILITY,
SEM_VERSION_REGEX,
SHORT_DESCRIPTION_PREFIX,
AUTHENTICATION_TYPE,
ORD_ACCESS_STRATEGY,
AUTH_TYPE_ORD_ACCESS_STRATEGY_MAP
SEM_VERSION_REGEX
};
10 changes: 5 additions & 5 deletions lib/defaults.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
const cds = require("@sap/cds");
const {
AUTHENTICATION_TYPE,
AUTH_TYPE_ORD_ACCESS_STRATEGY_MAP,
DESCRIPTION_PREFIX,
OPEN_RESOURCE_DISCOVERY_VERSION,
SHORT_DESCRIPTION_PREFIX,
AUTHENTICATION_TYPE,
AUTH_TYPE_ORD_ACCESS_STRATEGY_MAP,}
= require("./constants");
} = require("./constants");
const { getAuthenticationType } = require("./authentication");

const regexWithRemoval = (name) => {
return name?.replace(/[^a-zA-Z0-9]/g, "");
Expand Down Expand Up @@ -93,7 +93,7 @@ module.exports = {
url: "/open-resource-discovery/v1/documents/1",
accessStrategies: [
{
type: "open"// AUTH_TYPE_ORD_ACCESS_STRATEGY_MAP[cds.env.authentication.type] || AUTHENTICATION_TYPE.Open,
type: AUTH_TYPE_ORD_ACCESS_STRATEGY_MAP[getAuthenticationType()] || AUTHENTICATION_TYPE.Open
},
],
},
Expand Down
1 change: 0 additions & 1 deletion lib/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
module.exports = {
authenticate: require("./authentication.js"),
defaults: require("./defaults.js"),
getMetadata: require("./metaData.js"),
ord: require("./ord.js"),
Expand Down
17 changes: 14 additions & 3 deletions lib/plugin.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
const cds = require("@sap/cds");
const { Logger } = require("./logger");
const { authenticate, ord, getMetadata, defaults } = require("./");
const { ord, getMetadata, defaults, Logger } = require("./");
const { authenticate, getTrustedSubjects } = require("./authentication");

cds.on("bootstrap", async (app) => {
if (!cds.context) {
cds.context = {};
}

const trustedSubjects = await getTrustedSubjects();
cds.context.trustedSubjects = trustedSubjects;

cds.on("bootstrap", (app) => {
app.use("/.well-known/open-resource-discovery", async (req, res) => {
if (req.url === "/") {
res.status(200).send(defaults.baseTemplate);
Expand All @@ -29,4 +36,8 @@ cds.on("bootstrap", (app) => {
});
});

cds.on("shutdown", () => {
delete cds.context.trustedSubjects
});

module.exports = cds.server;
2 changes: 1 addition & 1 deletion xmpl/default-env.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"ORD_BASE_URL": "http://localhost:8080",
"ORD_SOURCE_TYPE": "local",
"ORD_DIRECTORY": "./example",
"ORD_AUTH": "basic",
"ORD_AUTH": "open",
"APP_USERS": {
"admin": "secret"
},
Expand Down

0 comments on commit 863f155

Please sign in to comment.