|
--- /home/sublime/workspace/node/express/server/original-server.js |
|
+++ /home/sublime/workspace/node/express/server/approov-protected-server.js |
|
@@ -1,4 +1,6 @@ |
|
-const debug = require('debug')('original-server') |
|
+const debug = require('debug')('approov-protected-server') |
|
+const jwt = require('express-jwt') |
|
+const crypto = require('crypto') |
|
const config = require('./configuration') |
|
const https = require('https') |
|
const fs = require('fs') |
|
@@ -41,6 +43,239 @@ |
|
} |
|
|
|
|
|
+//////////////////////////////////////////////////////////////////////////////// |
|
+/// YOUR APPLICATION CUSTOMIZABLE CALLBACKS FOR THE APPROOV INTEGRATION |
|
+//////////////////////////////////////////////////////////////////////////////// |
|
+/// |
|
+/// Feel free to customize this callbacks to best suite the needs your needs. |
|
+/// |
|
+ |
|
+// Callback to be customized with your preferred way of logging. |
|
+const logApproov = function(req, res, message) { |
|
+ debug(buildLogMessagePrefix(req, res) + ' ' + message) |
|
+} |
|
+ |
|
+// Callback to be personalized in order to get the claim value being used by |
|
+// your application. |
|
+// In the current scenario we use an Oauth2 token, but feel free to use what |
|
+// suits best your needs. |
|
+const getClaimValueFromRequest = function(req) { |
|
+ return req.get('oauth2-token') |
|
+} |
|
+ |
|
+// Callback to be customized with how you want to handle a request with an |
|
+// invalid Approov token. |
|
+// The code included in this callback is provided as an example, that you can |
|
+// keep or totally change it in a way that best suits your needs. |
|
+const handlesRequestWithInvalidApproovToken = function(err, req, res, next) { |
|
+ |
|
+ // Logging a message to make clear in the logs what was the action we took. |
|
+ // Feel free to skip it if you think is not necessary to your use case. |
|
+ let message = 'REQUEST WITH INVALID APPROOV TOKEN' |
|
+ |
|
+ if (config.approov.abortRequestOnInvalidToken === true) { |
|
+ message = 'REJECTED ' + message |
|
+ res.status(400) |
|
+ logApproov(req, res, message) |
|
+ res.json({}) |
|
+ return |
|
+ } |
|
+ |
|
+ message = 'ACCEPTED ' + message |
|
+ logApproov(req, res, message) |
|
+ next() |
|
+ return |
|
+} |
|
+ |
|
+// Callback to be customized with how you want to handle a request where the |
|
+// claim in the request doesn't match the custom payload claim in the Approov |
|
+// token. |
|
+// The code included in this callback is provided as an example, that you can |
|
+// keep or totally change it in a way that best suits your needs. |
|
+const handlesRequestWithInvalidClaimValue = function(req, res, next) { |
|
+ |
|
+ // Logging here to make clear in the logs what was the action we took. |
|
+ // Fseel free to skip it if you think is not necessary to your use case. |
|
+ let message = 'REQUEST WITH INVALID CLAIM VALUE' |
|
+ |
|
+ if (config.approov.abortRequestOnInvalidCustomPayloadClaim === true) { |
|
+ message = 'REJECTED ' + message |
|
+ res.status(400) |
|
+ logApproov(req, res, message) |
|
+ res.json({}) |
|
+ return |
|
+ } |
|
+ |
|
+ message = 'ACCEPTED ' + message |
|
+ logApproov(req, res, message) |
|
+ next() |
|
+ return |
|
+} |
|
+ |
|
+ |
|
+//////////////////////////////////////////////////////////////////////////////// |
|
+/// STARTS NON CUSTOMIZABLE LOGIC FOR THE APPROOV INTEGRATION |
|
+//////////////////////////////////////////////////////////////////////////////// |
|
+/// |
|
+/// This section contains code that is specific to the Approov integration, |
|
+/// thus we think that is not necessary to customize it, once is not |
|
+/// interfering with your application logic or behavior. |
|
+/// |
|
+ |
|
+////// APPROOV HELPER FUNCTIONS ////// |
|
+ |
|
+const isEmpty = function(value) { |
|
+ return (value === undefined) || (value === null) || (value === '') |
|
+} |
|
+ |
|
+const isString = function(value) { |
|
+ return (typeof(value) === 'string') |
|
+} |
|
+ |
|
+const isEmptyString = function(value) { |
|
+ return (isEmpty(value) === true) || (isString(value) === false) || (value.trim() === '') |
|
+} |
|
+ |
|
+ |
|
+////// APPROOV TOKEN ////// |
|
+ |
|
+ |
|
+// Callback that performs the Approov token check using the express-jwt library |
|
+const checkApproovToken = jwt({ |
|
+ secret: Buffer.from(config.approov.base64Secret, 'base64'), // decodes the Approov secret |
|
+ requestProperty: 'approovTokenDecoded', |
|
+ getToken: function fromApproovTokenHeader(req) { |
|
+ req.approovTokenError = false |
|
+ return req.get('approov-token') |
|
+ }, |
|
+ algorithms: ['HS256'] |
|
+}) |
|
+ |
|
+// Callback to handle the errors occurred while checking the Approov token. |
|
+const handlesApproovTokenError = function(err, req, res, next) { |
|
+ |
|
+ if (err.name === 'UnauthorizedError') { |
|
+ message = 'APPROOV TOKEN ERROR: ' + err |
|
+ logApproov(req, res, message) |
|
+ |
|
+ req.approovTokenError = true |
|
+ handlesRequestWithInvalidApproovToken(err, req, res, next) |
|
+ return |
|
+ } |
|
+ |
|
+ next() |
|
+ return |
|
+} |
|
+ |
|
+// Callback to handles when an Approov token is successfully validated. |
|
+const handlesApproovTokenSuccess = function(req, res, next) { |
|
+ if (req.approovTokenError === false) { |
|
+ logApproov(req, res, 'ACCEPTED REQUEST WITH VALID APPROOV TOKEN') |
|
+ } |
|
+ |
|
+ next() |
|
+ return |
|
+} |
|
+ |
|
+ |
|
+////// CUSTOM PAYLOAD CLAIN IN THE APPROOV TOKEN ////// |
|
+ |
|
+ |
|
+// Validates if the Approov contains the same claim has in the request |
|
+const isClaimValueInRequestValid = function(requestClaimValue, approovTokenDecoded) { |
|
+ |
|
+ if (isEmptyString(requestClaimValue)) { |
|
+ return false |
|
+ } |
|
+ |
|
+ if (isEmpty(approovTokenDecoded)) { |
|
+ return false |
|
+ } |
|
+ |
|
+ // checking if the approov token contains a custom payload claim and verify it. |
|
+ if (! isEmptyString(approovTokenDecoded.pay)) { |
|
+ |
|
+ const requestBase64ClaimValueHash = crypto.createHash('sha256').update(requestClaimValue, 'utf-8').digest('base64') |
|
+ |
|
+ return approovTokenDecoded.pay === requestBase64ClaimValueHash |
|
+ } |
|
+ |
|
+ // The Approov failover running in the Google cloud doesn't return the custom |
|
+ // payload claim, thus we always need to have a pass when is not present. |
|
+ return true |
|
+} |
|
+ |
|
+// Callback to check if the custom payload claim in an Approov token matches the |
|
+// claim in the request |
|
+const checkApproovTokenCustomPayloadClaim = function(req, res, next){ |
|
+ |
|
+ if (req.approovTokenError === true) { |
|
+ next() |
|
+ return |
|
+ } |
|
+ |
|
+ let message = 'REQUEST WITH VALID CLAIM VALUE' |
|
+ |
|
+ const requestClaimValue = getClaimValueFromRequest(req) |
|
+ |
|
+ if (isEmptyString(requestClaimValue)) { |
|
+ message = 'REQUEST WITHOUT A CLAIM VALUE' |
|
+ handlesRequestWithInvalidClaimValue(req, res, next) |
|
+ return |
|
+ } |
|
+ |
|
+ // checks if the claim from the request matches the custom payload claim in |
|
+ // the Approov token. |
|
+ const isValidClaim = isClaimValueInRequestValid(requestClaimValue, req.approovTokenDecoded) |
|
+ |
|
+ if (isValidClaim === false) { |
|
+ message = 'REQUEST WITH CLAIM VALUE NOT MATCHING THE CUSTOM PAYLOAD CLAIM IN THE APPROOV TOKEN' |
|
+ logApproov(req, res, message) |
|
+ handlesRequestWithInvalidClaimValue(req, res, next) |
|
+ return |
|
+ } |
|
+ |
|
+ |
|
+ message = 'ACCEPTED ' + message |
|
+ logApproov(req, res, message) |
|
+ next() |
|
+ return |
|
+} |
|
+ |
|
+/////// THE APPROOV INTERCEPTORS /////// |
|
+ |
|
+// Intercepts all calls to the shapes endpoint to validate the Approov token. |
|
+app.use('/shapes', checkApproovToken) |
|
+ |
|
+// Handles failure in validating the Approov token |
|
+app.use('/shapes', handlesApproovTokenError) |
|
+ |
|
+// Handles requests where the Approov token is a valid one. |
|
+app.use('/shapes', handlesApproovTokenSuccess) |
|
+ |
|
+// Intercepts all calls to the forms endpoint to validate the Approov token. |
|
+app.use('/forms', checkApproovToken) |
|
+ |
|
+// Handles failure in validating the Approov token |
|
+app.use('/forms', handlesApproovTokenError) |
|
+ |
|
+// Handles requests where the Approov token is a valid one. |
|
+app.use('/forms', handlesApproovTokenSuccess) |
|
+ |
|
+// checks if the custom payload claim is present in the Approov token and |
|
+// matches the claim used by the mobile app, that in this case we decided to be |
|
+// the ouath2 token, but you may want to use another type of claim. |
|
+app.use('/forms', checkApproovTokenCustomPayloadClaim) |
|
+ |
|
+/// NOTE: |
|
+/// Is important to place all the Approov interceptors before we declare the |
|
+/// endpoints of the API, otherwise they will not be able to intercept any |
|
+/// request. |
|
+ |
|
+//////////////////////////////////////////////////////////////////////////////// |
|
+/// ENDS APPOOV INTEGRATION |
|
+//////////////////////////////////////////////////////////////////////////////// |
|
+ |
|
//////////////// |
|
// ENDPOINTS |
|
//////////////// |
|
@@ -64,13 +299,11 @@ |
|
|
|
// shapes endpoint returns a random shape. |
|
app.get('/shapes', function(req, res, next) { |
|
- logResponseToRequest(req, res) |
|
res.json(getRandomShapeResponse()) |
|
}) |
|
|
|
// shapes endpoint returns a random form. |
|
app.get('/forms', function(req, res, next) { |
|
- logResponseToRequest(req, res) |
|
res.json(getRandomFormResponse()) |
|
}) |