Skip to content

Instantly share code, notes, and snippets.

@Exadra37
Last active May 1, 2019 14:54
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save Exadra37/ee31728307cc8eab838ca2dd8217f432 to your computer and use it in GitHub Desktop.
Save Exadra37/ee31728307cc8eab838ca2dd8217f432 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('/', checkApproovToken)
// Handles failure in validating the Approov token
app.use('/', handlesApproovTokenError)
// Handles requests where the Approov token is a valid one.
app.use('/', handlesApproovTokenSuccess)
// only use in the root endpoint `/` if you want to validate the custom payload
// claim in all endpoints, otherwise declare it per endpoint you want to protect.
app.use('/', checkApproovTokenCustomPayloadClaim)
// file: approov-protected-server.js
// 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)
// file: configuration.js
///////////////////////////
/// APPROOV ENVIRONMENT
//////////////////////////
let isToAbortRequestOnInvalidToken = true
let isToAbortOnInvalidClaim = true
let isApproovLoggingEnabled = true
const abortRequestOnInvalidToken = dotenv.parsed.APPROOV_ABORT_REQUEST_ON_INVALID_TOKEN || 'true'
const abortOnInvalidClaim = dotenv.parsed.APPROOV_ABORT_REQUEST_ON_INVALID_CUSTOM_PAYLOAD_CLAIM || 'true'
const approovLoggingEnabled = dotenv.parsed.APPROOV_LOGGING_ENABLED || 'true'
if (abortRequestOnInvalidToken.toLowerCase() === 'false') {
isToAbortRequestOnInvalidToken = false
}
if (abortOnInvalidClaim.toLowerCase() === 'false') {
isToAbortOnInvalidClaim = false
}
if (approovLoggingEnabled.toLowerCase() === 'false') {
isApproovLoggingEnabled = false
}
const approov = {
abortRequestOnInvalidToken: isToAbortRequestOnInvalidToken,
abortRequestOnInvalidCustomPayloadClaim: isToAbortOnInvalidClaim,
approovLoggingEnabled: isApproovLoggingEnabled,
// The Approov base64 secret must be retrieved from the Approov admin portal
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) {
req.approovTokenError = false
return req.get('approov-token')
},
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')
@@ -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())
})
// file: configuration.js
// if not already in use add:
require('dotenv').config()
// file: approov-protected-server.js
// 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
}
// file: approov-protected-server.js
// 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')
}
// file: approov-protected-server.js
// 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
}
// 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) {
// 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
}
// file: approov-protected-server.js
// 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
}
// 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