img

Introduction

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.

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}`);
});