Skip to content

Instantly share code, notes, and snippets.

@approovm
Forked from Exadra37/00-README.md
Last active June 2, 2022 09:43
Show Gist options
  • Save approovm/0f5356d74380ef1ff167face63cce05a to your computer and use it in GitHub Desktop.
Save approovm/0f5356d74380ef1ff167face63cce05a to your computer and use it in GitHub Desktop.
Code Snippets for an Approov Integration in a NodeJS Express API as per this blog post http://blog.approov.io/approov-integration-in-a-nodejs-express-api

APPROOV INTEGRATION IN A NODEJS EXPRESS API

The blog post can be found here.

TLDR

This walk-though will show us how simple it is to integrate Approov in a current API server using NodeJS and the Express framework.

We will see the requirements, dependencies and a step by step walk-through of the code necessary to implement Approov in a NodeJS Express API.

// file: approov-protected-server.js
// Intercepts all calls to the shapes endpoint to validate the Approov token.
app.use('/v2', checkApproovToken)
// Handles failure in validating the Approov token
app.use('/v2', handlesApproovTokenError)
// Handles requests where the Approov token is a valid one.
app.use('/v2', handlesApproovTokenSuccess)
// Checks if the Approov token binding is valid and aborts the request when the environment variable
// APPROOV_ABORT_REQUEST_ON_INVALID_TOKEN_BINDING is set to true in the environment file.
app.use('/v2', handlesApproovTokenBindingVerification)
// file: approov-protected-server.js
// Intercepts all calls to the shapes endpoint to validate the Approov token.
app.use('/v2/shapes', checkApproovToken)
// Handles failure in validating the Approov token
app.use('/v2/shapes', handlesApproovTokenError)
// Handles requests where the Approov token is a valid one.
app.use('/v2/shapes', handlesApproovTokenSuccess)
// Intercepts all calls to the forms endpoint to validate the Approov token.
app.use('/v2/forms', checkApproovToken)
// Handles failure in validating the Approov token
app.use('/v2/forms', handlesApproovTokenError)
// Handles requests where the Approov token is a valid one.
app.use('/v2/forms', handlesApproovTokenSuccess)
// Checks if the Approov token binding is valid and aborts the request when the environment variable
// APPROOV_ABORT_REQUEST_ON_INVALID_TOKEN_BINDING is set to true in the environment file.
app.use('/v2/forms', handlesApproovTokenBindingVerification)
// file: configuration.js
///////////////////////////
/// APPROOV ENVIRONMENT
//////////////////////////
let isToAbortRequestOnInvalidToken = true
let isToAbortOnInvalidBinding = true
let isApproovLoggingEnabled = true
const abortRequestOnInvalidToken = dotenv.parsed.APPROOV_ABORT_REQUEST_ON_INVALID_TOKEN || 'true'
const abortOnInvalidTokenBinding = dotenv.parsed.APPROOV_ABORT_REQUEST_ON_INVALID_TOKEN_BINDING || 'true'
const approovLoggingEnabled = dotenv.parsed.APPROOV_LOGGING_ENABLED || 'true'
if (abortRequestOnInvalidToken.toLowerCase() === 'false') {
isToAbortRequestOnInvalidToken = false
}
if (abortOnInvalidTokenBinding.toLowerCase() === 'false') {
isToAbortOnInvalidBinding = false
}
if (approovLoggingEnabled.toLowerCase() === 'false') {
isApproovLoggingEnabled = false
}
const approov = {
abortRequestOnInvalidToken: isToAbortRequestOnInvalidToken,
abortRequestOnInvalidTokenBinding: isToAbortOnInvalidBinding,
approovLoggingEnabled: isApproovLoggingEnabled,
// The Approov base64 secret must be retrieved with the Approov CLI tool
base64Secret: dotenv.parsed.APPROOV_BASE64_SECRET,
}
////////////////////////////
/// EXPORT CONFIGURATION
///////////////////////////
module.exports = {
server,
approov,
}
// file: approov-protected-server.js
// 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, res) {
req.approovTokenError = false
const approovToken = req.get('Approov-Token')
if (isEmptyString(approovToken)) {
req.approovTokenError = true
throw new Error('token empty or missing in the header of the request.')
}
return approovToken
},
algorithms: ['HS256']
})
--- /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')
@@ -60,6 +62,243 @@
}
+////////////////////////////////////////////////////////////////////////////////
+/// 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 token binding header value being used by
+// your application.
+// In the current scenario we use an Authorization token, but feel free to use what
+// suits best your needs.
+const getTokenBindingHeader = function(req) {
+ return req.get('Authorization')
+}
+
+// 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, httpStatusCode) {
+
+ logApproov(req, res, 'APPROOV TOKEN: ' + err)
+
+ // 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) {
+ buildBadRequestResponse(req, res, httpStatusCode, 'REJECTED ' + message)
+ return
+ }
+
+ message = 'ACCEPTED ' + message
+ logApproov(req, res, message)
+ next()
+ return
+}
+
+// Callback to be customized with how you want to handle a request where the
+// token binding in the request header doesn't match the the one 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 handlesRequestWithInvalidTokenBinding = function(req, res, next, httpStatusCode, message) {
+
+ logApproov(req, res, message)
+
+ // Logging here 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 logMessage = 'REQUEST WITH INVALID APPROOV TOKEN BINDING'
+
+ if (config.approov.abortRequestOnInvalidTokenBinding === true) {
+ buildBadRequestResponse(req, res, httpStatusCode, 'REJECTED ' + logMessage)
+ return
+ }
+
+ logApproov(req, res, 'ACCEPTED ' + logMessage)
+ next()
+ return
+}
+
+// Callback to build the response when a request fails to pass the Approov checks.
+const buildBadRequestResponse = function(req, res, httpStatusCode, logMessage) {
+ res.status(httpStatusCode)
+ logApproov(req, res, logMessage)
+ res.json({})
+}
+
+
+////////////////////////////////////////////////////////////////////////////////
+/// 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, res) {
+ req.approovTokenError = false
+ const approovToken = req.get('Approov-Token')
+
+ if (isEmptyString(approovToken)) {
+ req.approovTokenError = true
+ throw new Error('token empty or missing in the header of the request.')
+ }
+
+ return approovToken
+ },
+ algorithms: ['HS256']
+})
+
+// Callback to handle the errors occurred while checking the Approov token.
+const handlesApproovTokenError = function(err, req, res, next) {
+
+ if (req.approovTokenError === true) {
+ // When we reach here, it means the header `Approov-Token` is empty or is missing.
+ // @see checkApproovToken()
+ handlesRequestWithInvalidApproovToken(err, req, res, next, 400)
+ return
+ }
+
+ if (err.name === 'UnauthorizedError') {
+ // When we reach here, it means that an Error was thrown by the express-jwt
+ // library while decoding the Approov token.
+ // @see checkApproovToken()
+ req.approovTokenError = true
+ handlesRequestWithInvalidApproovToken(err, req, res, next, 401)
+ 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
+}
+
+
+////// APPROOV TOKEN BINDING //////
+
+
+// Callback to check the Approov token binding in the header matches with the one in the key `pay` of the Approov token claims.
+const handlesApproovTokenBindingVerification = function(req, res, next){
+
+ if (req.approovTokenError === true) {
+ next()
+ return
+ }
+
+ // The decoded Approov token was added to the request object when the checked it at `checkApproovToken()`
+ token_binding_payload = req.approovTokenDecoded.pay
+
+ if (token_binding_payload === undefined) {
+ logApproov(req, res, "APPROOV TOKEN BINDING WARNING: key 'pay' is missing.")
+ logApproov(req, res, 'ACCEPTED REQUEST WITH APPROOV TOKEN BINDING MISSING')
+ next()
+ return
+ }
+
+ if (isEmptyString(token_binding_payload)) {
+ handlesRequestWithInvalidTokenBinding(req, res, next, 400, "APPROOV TOKEN BINDING ERROR: key 'pay' in the decoded token is empty.")
+ return
+ }
+
+ // We use here the Authorization token, but feel free to use another header, but you need to bind this header to
+ // the Approov token in the mobile app.
+ const token_binding_header = getTokenBindingHeader(req)
+
+ if (isEmptyString(token_binding_header)) {
+ handlesRequestWithInvalidTokenBinding(req, res, next, 400, "APPROOV TOKEN BINDING ERROR: Missing or empty header to perform the verification for the token binding.")
+ return
+ }
+
+ // We need to hash and base64 encode the token binding header, because that's how it was included in the Approov
+ // token on the mobile app.
+ const token_binding_header_encoded = crypto.createHash('sha256').update(token_binding_header, 'utf-8').digest('base64')
+
+ if (token_binding_payload !== token_binding_header_encoded) {
+ handlesRequestWithInvalidTokenBinding(req, res, next, 401, "APPROOV TOKEN BINDING ERROR: token binding in header doesn't match with the key 'pay' in the decoded token.")
+ return
+ }
+
+ logApproov(req, res, 'ACCEPTED REQUEST WITH VALID APPROOV TOKEN BINDING')
+
+ // Let the request continue as usual.
+ next()
+ return
+}
+
+/////// THE APPROOV INTERCEPTORS ///////
+
+// Intercepts all calls to the shapes endpoint to validate the Approov token.
+app.use('/v2/shapes', checkApproovToken)
+
+// Handles failure in validating the Approov token
+app.use('/v2/shapes', handlesApproovTokenError)
+
+// Handles requests where the Approov token is a valid one.
+app.use('/v2/shapes', handlesApproovTokenSuccess)
+
+// Intercepts all calls to the forms endpoint to validate the Approov token.
+app.use('/v2/forms', checkApproovToken)
+
+// Handles failure in validating the Approov token
+app.use('/v2/forms', handlesApproovTokenError)
+
+// Handles requests where the Approov token is a valid one.
+app.use('/v2/forms', handlesApproovTokenSuccess)
+
+// Checks if the Approov token binding is valid and aborts the request when the environment variable
+// APPROOV_ABORT_REQUEST_ON_INVALID_TOKEN_BINDING is set to true in the environment file.
+app.use('/v2/forms', handlesApproovTokenBindingVerification)
+
+/// 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
////////////////
@@ -84,6 +323,28 @@
app.get('/v1/forms', function(req, res, next) {
logResponseToRequest(req, res)
buildFormsResponse(res, 'unprotected')
+})
+
+/**
+ * V2 ENDPOINTS
+ */
+
+// simple 'hello world' endpoint.
+app.get('/v2/hello', function (req, res, next) {
+ logResponseToRequest(req, res)
+ buildHelloWorldResponse(res)
+})
+
+// shapes endpoint returns a random shape.
+app.get('/v2/shapes', function(req, res, next) {
+ logResponseToRequest(req, res)
+ buildShapesResponse(res, 'protected')
+})
+
+// shapes endpoint returns a random form.
+app.get('/v2/forms', function(req, res, next) {
+ logResponseToRequest(req, res)
+ buildFormsResponse(res, 'protected')
})
// file: configuration.js
// if not already in use add:
require('dotenv').config()
// file: approov-protected-server.js
// Callback to be personalized in order to get the token binding header value being used by
// your application.
// In the current scenario we use an Authorization token, but feel free to use what
// suits best your needs.
const getTokenBindingHeader = function(req) {
return req.get('Authorization')
}
// file: approov-protected-server.js
// Callback to check the Approov token binding in the header matches with the one in the key `pay` of the Approov token claims.
const handlesApproovTokenBindingVerification = function(req, res, next){
if (req.approovTokenError === true) {
next()
return
}
// The decoded Approov token was added to the request object when the checked it at `checkApproovToken()`
token_binding_payload = req.approovTokenDecoded.pay
if (token_binding_payload === undefined) {
logApproov(req, res, "APPROOV TOKEN BINDING WARNING: key 'pay' is missing.")
logApproov(req, res, 'ACCEPTED REQUEST WITH APPROOV TOKEN BINDING MISSING')
next()
return
}
if (isEmptyString(token_binding_payload)) {
handlesRequestWithInvalidTokenBinding(req, res, next, 400, "APPROOV TOKEN BINDING ERROR: key 'pay' in the decoded token is empty.")
return
}
// We use here the Authorization token, but feel free to use another header, but you need to bind this header to
// the Approov token in the mobile app.
const token_binding_header = getTokenBindingHeader(req)
if (isEmptyString(token_binding_header)) {
handlesRequestWithInvalidTokenBinding(req, res, next, 400, "APPROOV TOKEN BINDING ERROR: Missing or empty header to perform the verification for the token binding.")
return
}
// We need to hash and base64 encode the token binding header, because that's how it was included in the Approov
// token on the mobile app.
const token_binding_header_encoded = crypto.createHash('sha256').update(token_binding_header, 'utf-8').digest('base64')
if (token_binding_payload !== token_binding_header_encoded) {
handlesRequestWithInvalidTokenBinding(req, res, next, 401, "APPROOV TOKEN BINDING ERROR: token binding in header doesn't match with the key 'pay' in the decoded token.")
return
}
logApproov(req, res, 'ACCEPTED REQUEST WITH VALID APPROOV TOKEN BINDING')
// Let the request continue as usual.
next()
return
}
// file: approov-protected-server.js
// Callback to handle the errors occurred while checking the Approov token.
const handlesApproovTokenError = function(err, req, res, next) {
if (req.approovTokenError === true) {
// When we reach here, it means the header `Approov-Token` is empty or is missing.
// @see checkApproovToken()
handlesRequestWithInvalidApproovToken(err, req, res, next, 400)
return
}
if (err.name === 'UnauthorizedError') {
// When we reach here, it means that an Error was thrown by the express-jwt
// library while decoding the Approov token.
// @see checkApproovToken()
req.approovTokenError = true
handlesRequestWithInvalidApproovToken(err, req, res, next, 401)
return
}
next()
return
}
// file: approov-protected-server.js
// 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
}
// file: approov-protected-server.js
// 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, httpStatusCode) {
logApproov(req, res, 'APPROOV TOKEN: ' + err)
// 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) {
buildBadRequestResponse(req, res, httpStatusCode, 'REJECTED ' + message)
return
}
message = 'ACCEPTED ' + message
logApproov(req, res, message)
next()
return
}
message = 'ACCEPTED ' + message
logApproov(req, res, message)
next()
return
}
// file: approov-protected-server.js
// Callback to be customized with how you want to handle a request where the
// token binding in the request header doesn't match the the one 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 handlesRequestWithInvalidTokenBinding = function(req, res, next, httpStatusCode, message) {
logApproov(req, res, message)
// Logging here 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 logMessage = 'REQUEST WITH INVALID APPROOV TOKEN BINDING'
if (config.approov.abortRequestOnInvalidTokenBinding === true) {
buildBadRequestResponse(req, res, httpStatusCode, 'REJECTED ' + logMessage)
return
}
logApproov(req, res, 'ACCEPTED ' + logMessage)
next()
return
}
// file: approov-protected-server.js
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() === '')
}
// file: approov-protected-server.js
// Callback to be customized with your preferred way of logging.
const logApproov = function(req, res, message) {
debug(buildLogMessagePrefix(req, res) + ' ' + message)
}
// file: approov-protected-server.js
const jwt = require('express-jwt')
const crypto = require('crypto')
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment