Proxy
Introduction
-
The app is a frontend application: all the business logic, interface, authentication, and reading and modifying of Sage Active data are performed on the browser side.
-
However, a frontend application cannot directly call Sage Active Public API V2 due to the CORS policy since the domain of the application differs from that of the API, and the browser will block access for security reasons.
Therefore, API calls must be made from the backend, and this backend will authorize the frontend application to communicate with it. -
Also, it is not recommended to store the x-api-key (How to find?) in the frontend application’s sources.
This value should be stored on the backend. -
Finally, although the ClientId of the application can be stored in the frontend app’s sources, it is more logical to store it on the backend.
For these reasons, the frontend application must communicate with a backend proxy that will be responsible for redirecting the queries and mutations requested by the frontend to the public API.
This proxy will also manage the x-api-key and clientId values in its settings.
x-api-env instead of x-api-key
Normally, for each public API call, it’s necessary to include an x-api-key (How to find?) in the request header.
However, in this scenario, the frontend application does not directly call the public API but instead communicates with a proxy.
This proxy then redirects the requests to the public API.
As a result, the frontend does not pass an x-api-key but an x-api-env in the header.
The x-api-env represents a Sage Active environment (such as STAGE, Prod FR, Prod ES, or Prod DE).
When the proxy receives a request, it associates the value of x-api-env with the correct x-api-key and the corresponding public API URL.
This approach ensures that the frontend application can seamlessly interact with different Sage Active environments through the proxy, without directly managing API keys, thus enhancing security and simplifying the frontend code.
Example of a Node.js Proxy
Below, we describe an example of a Node.js proxy, on which the Sample quotes management app’s proxy is based.
You are free to design this proxy with your preferred technology and organize it as you see fit.
The proxy relies on settings, so below is the local representation in the Node.js configuration file .env
.
.env
# List of authorized frontend domains.
ALLOWED_ORIGINS=http://127.0.0.1:4000,https://myapp.com,https://myappstg.com
# Frontend domains with wildcard (e.g. https://pr201.devmyapp.com, https://pr202.devmyapp.com)
ALLOWED_ORIGIN_DEV=^https:\/\/[\w-]+\.devmyapp\.com$
# Authorized list of Sage Active environments:
ALLOWED_ENVS=stage,fr,es,de
# x-api-key for the SBCAuth "MyApp" application for all public APIs of STAGE, and the 3 PRODS.
API_KEY_STAGE=c650e4450d2045e7bxxxxxxxxxxxx
API_KEY_FR=3050ba8a1fa446df9fbdxxxxxxxxxxxx
API_KEY_ES=f3f80e0a977449c8a0bdxxxxxxxxxxxx
API_KEY_DE=30e246460c96427a9733xxxxxxxxxxxx
# ClientId for the SBCAuth "MyApp" application for all public APIs of STAGE, and the 3 PRODS.
CLIENTID_STAGE=26zkb7R34Tdds9J2xxxxxxxxxxxx
CLIENTID_FR=aNfc5eMcRbfafcB9eFVxxxxxxxxxxxx
CLIENTID_ES=daOg0149Bc4PY46um3xxxxxxxxxxxxx
CLIENTID_DE=c6i4E5BdqczAJg9d4Kxxxxxxxxxxxxx
Below we now describe the source code of the proxy in the file proxy-server-apipub.js.
-
proxy-server-apipub.js
- Initial Setup: Configures environment variables, Express, CORS, and HTTP proxy middleware.
- CORS Configuration: Sets CORS options based on allowed origins, handling CORS for development and production environments.
- Express App Initialization: Creates an Express app with CORS enabled and sets up options for preflight requests.
- Environment Information Endpoint: A GET endpoint ‘/api/environments’ to provide filtered environment and authentication configurations based on headers and allowed environments.
determineApiTarget(env)
: Determines the API target URL based on the environment.- Environment and Authentication Configuration: Defines mappings for Sage Active environments and corresponding authentication and API configurations.
getApiKeyForEnv(env)
: Retrieves API keys for different environments.- API Proxy Middleware: Sets up a middleware to proxy API requests, handling environment-specific target URLs, API keys, and custom response handling.
- Server Initialization: Starts the Express server on a specified port, defaulting to 3000.
require('dotenv').config();
const express = require('express');
const cors = require('cors');
const { createProxyMiddleware } = require('http-proxy-middleware');
const app = express();
const apiConfigCache = {};
const allowedOrigins = process.env.ALLOWED_ORIGINS.split(',');
const allowedOriginDev = new RegExp(process.env.ALLOWED_ORIGIN_DEV);
let allowedSageActiveEnvs;
let isInternal = false;
const corsOptions = {
origin: function (origin, callback) {
if (allowedOrigins.includes(origin) || (allowedOriginDev && allowedOriginDev.test(origin))) {
callback(null, true);
} else {
callback(new Error(`${origin} not allowed by CORS Proxy policy`));
}
}
};
app.use(cors(corsOptions));
app.options('*', cors(corsOptions));
//Allows the front to retrieve environments info
app.get('/api/environments', (req, res) => {
isInternal = req.headers['x-frontend-url'].includes('/internal/');
allowedSageActiveEnvs = isInternal ? process.env.ALLOWED_ENVS_INTERNAL.split(',') : process.env.ALLOWED_ENVS.split(',');
const filteredSbcAuthEnvBySageActiveEnv = Object.keys(sbcAuthEnvBySageActiveEnv)
.filter(key => allowedSageActiveEnvs.includes(key))
.reduce((obj, key) => {
obj[key] = sbcAuthEnvBySageActiveEnv[key];
return obj;
}, {});
const filteredSbcAuthConfigBySageActiveEnvs = Object.keys(sbcAuthConfigBySageActiveEnvs)
.filter(key => Object.values(filteredSbcAuthEnvBySageActiveEnv).includes(key))
.reduce((obj, key) => {
obj[key] = sbcAuthConfigBySageActiveEnvs[key];
return obj;
}, {});
res.json({
allowedSageActiveEnvs: allowedSageActiveEnvs,
sbcAuthConfigBySageActiveEnvs: filteredSbcAuthConfigBySageActiveEnvs,
sbcAuthEnvBySageActiveEnv: filteredSbcAuthEnvBySageActiveEnv
});
});
function determineApiTarget(env) {
if (!allowedSageActiveEnvs.includes('*') && !allowedSageActiveEnvs.includes(env)) {
throw new Error(`Environment ${env} is not allowed`);
}
return `https://api.${env}.active.sage.com/graphql`;
}
const sbcAuthEnvBySageActiveEnv = {
'stage':'stage',
'fr':'prodFR',
'es':'prodES',
'de':'prodDE'
};
const apiKeysByEnv = {
'stage': process.env.API_KEY_STAGE,
'fr': process.env.API_KEY_FR,
'es': process.env.API_KEY_ES,
'de': process.env.API_KEY_DE
};
const sbcAuthConfigBySageActiveEnvs = {
'stage': {
clientId: process.env.CLIENTID_STAGE,
sbcAuthUrl: "https://stg-sbcauth.sage.fr"
},
'prodFR': {
clientId: process.env.CLIENTID_FR,
sbcAuthUrl: "https://sbcauth.sage.fr"
},
'prodES': {
clientId: process.env.CLIENTID_ES,
sbcAuthUrl: "https://sbcauth.sage.fr"
},
'prodDE': {
clientId: process.env.CLIENTID_DE,
sbcAuthUrl: "https://sbcauth.sage.fr"
}
};
function getApiKeyForEnv(env) {
return apiKeysByEnv[env] || '';
}
app.use('/api', (req, res, next) => {
const env = req.header('x-sageactive-env').toLowerCase();
if (!apiConfigCache[env]) {
const target = determineApiTarget(env);
const apiKey = getApiKeyForEnv(env);
apiConfigCache[env] = { target, apiKey };
}
const { target, apiKey } = apiConfigCache[env];
const proxy = createProxyMiddleware({
target: target,
changeOrigin: true,
pathRewrite: {'^/api': ''},
onProxyReq: (proxyReq, req, res) => {
proxyReq.removeHeader('x-sageactive-env');
proxyReq.removeHeader('x-original-url');
if (apiKey)
proxyReq.setHeader('x-api-key', apiKey);
else
proxyReq.removeHeader('x-api-key');
},
selfHandleResponse: true,
onProxyRes: (proxyRes, req, res) => {
let body = [];
proxyRes.on('data', chunk => {
body.push(chunk);
});
proxyRes.on('end', () => {
body = Buffer.concat(body);
const bodyString = body.toString('utf8');
res.writeHead(proxyRes.statusCode, proxyRes.headers);
res.end(bodyString);
});
},
});
proxy(req, res, next);
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server is running on port ${PORT}`);
});