From cbbd04f6741670f9e715ea50537fde31c495cf3a Mon Sep 17 00:00:00 2001 From: kmilejMAC Date: Fri, 16 Jan 2026 15:24:48 +0000 Subject: [PATCH 1/8] Initial working Node.js --- .env.example | 11 + .gitignore | 2 + ApproovApplication.js | 470 ++++++++++++++++++ Dockerfile | 53 +- EXAMPLES.md | 114 ----- OVERVIEW.md | 55 -- QUICKSTARTS.md | 51 -- README.md | 270 ++++++---- TESTING.md | 43 -- docker-compose.yml | 37 -- docs/APPROOV_TOKEN_BINDING_QUICKSTART.md | 243 --------- docs/APPROOV_TOKEN_QUICKSTART.md | 193 ------- package-lock.json | 15 + package.json | 14 + run_server.sh | 6 + scripts/build.sh | 87 ++++ scripts/install-prerequisites.sh | 32 ++ .../token-binding-check/.env.example | 12 - .../token-binding-check/README.md | 102 ---- .../hello-server-protected.js | 101 ---- .../token-binding-check/package-lock.json | 218 -------- .../token-binding-check/package.json | 17 - .../token-check/.env.example | 12 - .../token-check/README.md | 101 ---- .../token-check/hello-server-protected.js | 66 --- .../token-check/package-lock.json | 218 -------- .../token-check/package.json | 17 - src/unprotected-server/.env.example | 5 - src/unprotected-server/README.md | 98 ---- .../hello-server-unprotected.js | 22 - src/unprotected-server/package-lock.json | 32 -- src/unprotected-server/package.json | 16 - test.sh | 382 ++++++++++++++ 33 files changed, 1215 insertions(+), 1900 deletions(-) create mode 100644 .env.example create mode 100644 ApproovApplication.js delete mode 100644 EXAMPLES.md delete mode 100644 OVERVIEW.md delete mode 100644 QUICKSTARTS.md delete mode 100644 TESTING.md delete mode 100644 docker-compose.yml delete mode 100644 docs/APPROOV_TOKEN_BINDING_QUICKSTART.md delete mode 100644 docs/APPROOV_TOKEN_QUICKSTART.md create mode 100644 package-lock.json create mode 100644 package.json create mode 100755 run_server.sh create mode 100755 scripts/build.sh create mode 100755 scripts/install-prerequisites.sh delete mode 100644 src/approov-protected-server/token-binding-check/.env.example delete mode 100644 src/approov-protected-server/token-binding-check/README.md delete mode 100644 src/approov-protected-server/token-binding-check/hello-server-protected.js delete mode 100644 src/approov-protected-server/token-binding-check/package-lock.json delete mode 100644 src/approov-protected-server/token-binding-check/package.json delete mode 100644 src/approov-protected-server/token-check/.env.example delete mode 100644 src/approov-protected-server/token-check/README.md delete mode 100644 src/approov-protected-server/token-check/hello-server-protected.js delete mode 100644 src/approov-protected-server/token-check/package-lock.json delete mode 100644 src/approov-protected-server/token-check/package.json delete mode 100644 src/unprotected-server/.env.example delete mode 100644 src/unprotected-server/README.md delete mode 100644 src/unprotected-server/hello-server-unprotected.js delete mode 100644 src/unprotected-server/package-lock.json delete mode 100644 src/unprotected-server/package.json create mode 100644 test.sh diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..8817383 --- /dev/null +++ b/.env.example @@ -0,0 +1,11 @@ +# HTTP port the backend listens on +HTTP_PORT=8080 + +# Approov secret: approov secret -get base64url +APPROOV_BASE64URL_SECRET=approov_base64url_secret_here + +# Localhost +SERVER_HOSTNAME=0.0.0.0 + +# Command that starts your server inside the container +APP_START_CMD=npm start \ No newline at end of file diff --git a/.gitignore b/.gitignore index 2d7ec5c..a1fe691 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ .env node_modules/ +.config/ +.DS_Store diff --git a/ApproovApplication.js b/ApproovApplication.js new file mode 100644 index 0000000..bd92aa7 --- /dev/null +++ b/ApproovApplication.js @@ -0,0 +1,470 @@ +'use strict'; + +const http = require('http'); +const crypto = require('crypto'); +const fs = require('fs'); +const path = require('path'); + +loadEnvFile(path.join(__dirname, '.env')); + +const SERVER_HOSTNAME = process.env.SERVER_HOSTNAME || 'localhost'; +const HTTP_PORT = parsePort(process.env.HTTP_PORT, 8080); + +const APPROOV_SECRET_BASE64URL = + process.env.APPROOV_BASE64URL_SECRET || process.env.APPROOV_BASE64_SECRET; + +if (!hasText(APPROOV_SECRET_BASE64URL)) { + console.error('APPROOV_BASE64URL_SECRET environment variable is not set'); + process.exit(1); +} + +const APPROOV_SECRET = base64UrlDecodeToBuffer(APPROOV_SECRET_BASE64URL.trim()); + +let approovEnabled = true; +let tokenBindingEnabled = true; + +const HEADER_NAMES = Object.freeze({ + APPROOV_TOKEN: 'approov-token', + AUTHORIZATION: 'authorization', + CONTENT_DIGEST: 'content-digest', +}); + +const ROUTES = Object.freeze([ + { method: 'GET', path: '/', handler: homeHandler, requiresApproov: false }, + { + method: 'GET', + path: '/approov-state', + handler: approovStateHandler, + requiresApproov: false, + }, + { + method: 'POST', + path: '/approov/enable', + handler: enableApproovHandler, + requiresApproov: false, + }, + { + method: 'POST', + path: '/approov/disable', + handler: disableApproovHandler, + requiresApproov: false, + }, + { + method: 'POST', + path: '/token-binding/enable', + handler: enableTokenBindingHandler, + requiresApproov: false, + }, + { + method: 'POST', + path: '/token-binding/disable', + handler: disableTokenBindingHandler, + requiresApproov: false, + }, + { + method: 'GET', + path: '/unprotected', + handler: unprotectedHandler, + requiresApproov: false, + }, + { + method: 'GET', + path: '/token-check', + handler: tokenCheckHandler, + requiresApproov: true, + }, + { + method: 'GET', + path: '/token-binding', + handler: tokenBindingHandler, + requiresApproov: true, + }, + { + method: 'GET', + path: '/token-double-binding', + handler: tokenDoubleBindingHandler, + requiresApproov: true, + }, +]); + +const ROUTE_TABLE = new Map( + ROUTES.map((route) => [`${route.method} ${route.path}`, route]) +); + +const MIDDLEWARE = Object.freeze([approovTokenVerifier]); + +const server = http.createServer((req, res) => { + const requestInfo = parseRequest(req); + const route = ROUTE_TABLE.get(`${requestInfo.method} ${requestInfo.path}`); + + if (!route) { + writeJson(res, 404, { error: 'not_found' }); + return; + } + + const ctx = { + req, + res, + route, + method: requestInfo.method, + path: requestInfo.path, + headers: req.headers, + state: {}, + }; + + try { + runMiddleware(ctx, MIDDLEWARE, route.handler); + } catch (error) { + console.error('Unhandled error:', error); + writeJson(res, 500, { error: 'server_error' }); + } +}); + +server.listen(HTTP_PORT, SERVER_HOSTNAME, () => { + console.log( + `Approov demo API running at http://${SERVER_HOSTNAME}:${HTTP_PORT}` + ); +}); + +function homeHandler(ctx) { + writeJson( + ctx.res, + 200, + infoPayload(`Approov demo API is running on port ${HTTP_PORT}.`) + ); +} + +function approovStateHandler(ctx) { + writeJson(ctx.res, 200, statePayload()); +} + +function enableApproovHandler(ctx) { + approovEnabled = true; + tokenBindingEnabled = true; + writeJson(ctx.res, 200, statePayload()); +} + +function disableApproovHandler(ctx) { + approovEnabled = false; + tokenBindingEnabled = false; + writeJson(ctx.res, 200, statePayload()); +} + +function enableTokenBindingHandler(ctx) { + tokenBindingEnabled = true; + writeJson(ctx.res, 200, statePayload()); +} + +function disableTokenBindingHandler(ctx) { + tokenBindingEnabled = false; + writeJson(ctx.res, 200, statePayload()); +} + +function unprotectedHandler(ctx) { + writeJson( + ctx.res, + 200, + infoPayload("Unprotected endpoint '/unprotected'; no Approov checks performed.") + ); +} + +function tokenCheckHandler(ctx) { + writeJson( + ctx.res, + 200, + infoPayload("Protected endpoint '/token-check'; Approov token verified.") + ); +} + +function tokenBindingHandler(ctx) { + const authorization = headerValue(ctx.headers, HEADER_NAMES.AUTHORIZATION); + const response = infoPayload( + "Protected endpoint '/token-binding'; Approov token binding enforced." + ); + response.authorizationHeaderPresent = hasText(authorization); + writeJson(ctx.res, 200, response); +} + +function tokenDoubleBindingHandler(ctx) { + const authorization = headerValue(ctx.headers, HEADER_NAMES.AUTHORIZATION); + const contentDigest = headerValue(ctx.headers, HEADER_NAMES.CONTENT_DIGEST); + const response = infoPayload( + "Protected endpoint '/token-double-binding'; dual token binding enforced." + ); + response.authorizationHeaderPresent = hasText(authorization); + response.contentDigestHeaderPresent = hasText(contentDigest); + writeJson(ctx.res, 200, response); +} + +function approovTokenVerifier(ctx, next) { + const route = ctx.route; + if (!route || !route.requiresApproov) { + next(); + return; + } + + if (!approovEnabled) { + next(); + return; + } + + const token = readApproovToken(ctx.headers); + if (!hasText(token)) { + unauthorized(ctx.res, 'Missing Approov-Token header.'); + return; + } + + let claims; + try { + claims = verifyApproovToken(token); + } catch (error) { + unauthorized(ctx.res, error.message); + return; + } + + if (tokenBindingEnabled && needsBindingCheck(route.path)) { + const bindingValue = extractBindingValue(route.path, ctx.headers); + if (!hasText(bindingValue) || !isBindingValid(bindingValue, claims)) { + unauthorized(ctx.res, 'Invalid token binding.'); + return; + } + } + + ctx.state.approovClaims = claims; + next(); +} + +function verifyApproovToken(token) { + const parsed = parseJwt(token); + + if (parsed.header.alg !== 'HS256') { + throw new Error(`Unsupported token alg: ${parsed.header.alg || 'unknown'}.`); + } + + const expectedSignature = signHmac(parsed.signingInput, APPROOV_SECRET); + if (!bufferEquals(expectedSignature, parsed.signature)) { + throw new Error('Approov token signature verification failed.'); + } + + validateExpiration(parsed.payload); + return parsed.payload; +} + +function parseJwt(token) { + const parts = token.split('.'); + if (parts.length !== 3) { + throw new Error('Approov token is not a valid JWT.'); + } + + const [headerB64, payloadB64, signatureB64] = parts; + const headerJson = base64UrlDecodeToString(headerB64); + const payloadJson = base64UrlDecodeToString(payloadB64); + const signature = base64UrlDecodeToBuffer(signatureB64); + + return { + header: parseJson(headerJson, 'header'), + payload: parseJson(payloadJson, 'payload'), + signature, + signingInput: `${headerB64}.${payloadB64}`, + }; +} + +function needsBindingCheck(pathname) { + return pathname === '/token-binding' || pathname === '/token-double-binding'; +} + +function extractBindingValue(pathname, headers) { + if (pathname === '/token-binding') { + return trimOrNull(headerValue(headers, HEADER_NAMES.AUTHORIZATION)); + } + + const authorization = trimOrNull( + headerValue(headers, HEADER_NAMES.AUTHORIZATION) + ); + const digest = trimOrNull( + headerValue(headers, HEADER_NAMES.CONTENT_DIGEST) + ); + + if (!hasText(authorization) || !hasText(digest)) { + return null; + } + + return authorization + digest; +} + +function isBindingValid(bindingValue, claims) { + const expected = typeof claims.pay === 'string' ? claims.pay.trim() : ''; + if (!hasText(expected)) { + return false; + } + + const computed = hashBase64(bindingValue); + return safeStringEqual(expected, computed); +} + +function validateExpiration(claims) { + const exp = Number(claims.exp); + if (!Number.isFinite(exp)) { + throw new Error('Approov token missing exp claim.'); + } + + const now = Math.floor(Date.now() / 1000); + if (exp <= now) { + throw new Error('Approov token expired.'); + } +} + +function statePayload() { + return { + approovEnabled, + tokenBindingEnabled, + }; +} + +function infoPayload(details) { + return { + ...statePayload(), + details, + }; +} + +function readApproovToken(headers) { + return trimOrNull(headerValue(headers, HEADER_NAMES.APPROOV_TOKEN)); +} + +function headerValue(headers, name) { + const value = headers[name]; + return Array.isArray(value) ? value[0] : value; +} + +function runMiddleware(ctx, middlewares, handler) { + let index = -1; + + const dispatch = () => { + index += 1; + if (index < middlewares.length) { + middlewares[index](ctx, dispatch); + return; + } + handler(ctx); + }; + + dispatch(); +} + +function parseRequest(req) { + const host = req.headers.host || `${SERVER_HOSTNAME}:${HTTP_PORT}`; + const url = new URL(req.url || '/', `http://${host}`); + return { + path: url.pathname, + method: (req.method || 'GET').toUpperCase(), + }; +} + +function unauthorized(res, message) { + writeJson(res, 401, { error: 'unauthorized', message }); +} + +function writeJson(res, statusCode, payload) { + const body = JSON.stringify(payload); + res.writeHead(statusCode, { + 'Content-Type': 'application/json; charset=utf-8', + 'Cache-Control': 'no-cache', + 'Content-Length': Buffer.byteLength(body), + }); + res.end(body); +} + +function signHmac(value, secret) { + return crypto.createHmac('sha256', secret).update(value).digest(); +} + +function hashBase64(value) { + return crypto + .createHash('sha256') + .update(value, 'utf8') + .digest('base64'); +} + +function bufferEquals(left, right) { + if (!Buffer.isBuffer(left) || !Buffer.isBuffer(right)) { + return false; + } + if (left.length !== right.length) { + return false; + } + return crypto.timingSafeEqual(left, right); +} + +function safeStringEqual(left, right) { + if (!hasText(left) || !hasText(right)) { + return false; + } + return bufferEquals(Buffer.from(left), Buffer.from(right)); +} + +function parseJson(value, label) { + try { + return JSON.parse(value); + } catch (error) { + throw new Error(`Approov token ${label} is not valid JSON.`); + } +} + +function base64UrlDecodeToString(value) { + return base64UrlDecodeToBuffer(value).toString('utf8'); +} + +function base64UrlDecodeToBuffer(value) { + const normalized = value.replace(/-/g, '+').replace(/_/g, '/'); + const padded = normalized.padEnd( + normalized.length + ((4 - (normalized.length % 4)) % 4), + '=' + ); + return Buffer.from(padded, 'base64'); +} + +function parsePort(value, fallback) { + if (!hasText(value)) { + return fallback; + } + const port = Number.parseInt(value, 10); + return Number.isFinite(port) ? port : fallback; +} + +function hasText(value) { + return typeof value === 'string' && value.trim().length > 0; +} + +function trimOrNull(value) { + return value == null ? null : value.trim(); +} + +function loadEnvFile(filePath) { + if (!fs.existsSync(filePath)) { + return; + } + + const content = fs.readFileSync(filePath, 'utf8'); + content.split(/\r?\n/).forEach((line) => { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith('#')) { + return; + } + const index = trimmed.indexOf('='); + if (index <= 0) { + return; + } + const key = trimmed.slice(0, index).trim(); + let value = trimmed.slice(index + 1).trim(); + if ( + (value.startsWith('"') && value.endsWith('"')) || + (value.startsWith("'") && value.endsWith("'")) + ) { + value = value.slice(1, -1); + } + if (process.env[key] === undefined) { + process.env[key] = value; + } + }); +} diff --git a/Dockerfile b/Dockerfile index af659ec..c737cd6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,47 +1,16 @@ -ARG TAG=17.7.1-slim +# syntax=docker/dockerfile:1 +# Builds the quickstart backend container image and configures scripts/install-prerequisites.sh and scripts/build.sh +# as the entrypoint used both locally and when deployed via Docker. +FROM node:22-slim -FROM node:${TAG} +ENV APP_HOME=/workspace \ + RUN_MODE=container -ARG CONTAINER_USER="node" -ARG LANGUAGE_CODE="en" -ARG COUNTRY_CODE="GB" -ARG ENCODING="UTF-8" +WORKDIR /app -ARG LOCALE_STRING="${LANGUAGE_CODE}_${COUNTRY_CODE}" -ARG LOCALIZATION="${LOCALE_STRING}.${ENCODING}" +COPY . . -ARG OH_MY_ZSH_THEME="bira" +RUN npm install -RUN apt update && apt -y upgrade && \ - apt -y install \ - locales \ - git \ - curl \ - inotify-tools \ - zsh && \ - - echo "${LOCALIZATION} ${ENCODING}" > /etc/locale.gen && \ - locale-gen "${LOCALIZATION}" && \ - - # useradd -m -u 1000 -s /usr/bin/zsh "${CONTAINER_USER}" && \ - - bash -c "$(curl -fsSL https://raw.githubusercontent.com/robbyrussell/oh-my-zsh/master/tools/install.sh)" && \ - - cp -v /root/.zshrc /home/"${CONTAINER_USER}"/.zshrc && \ - cp -rv /root/.oh-my-zsh /home/"${CONTAINER_USER}"/.oh-my-zsh && \ - sed -i "s/\/root/\/home\/${CONTAINER_USER}/g" /home/"${CONTAINER_USER}"/.zshrc && \ - sed -i s/ZSH_THEME=\"robbyrussell\"/ZSH_THEME=\"${OH_MY_ZSH_THEME}\"/g /home/${CONTAINER_USER}/.zshrc && \ - mkdir /home/"${CONTAINER_USER}"/workspace && \ - chown -R "${CONTAINER_USER}":"${CONTAINER_USER}" /home/"${CONTAINER_USER}" - -USER ${CONTAINER_USER} - -ENV USER ${CONTAINER_USER} -ENV LANG "${LOCALIZATION}" -ENV LANGUAGE "${LOCALE_STRING}:${LANGUAGE_CODE}" -ENV PATH=/home/${CONTAINER_USER}/.local/bin:${PATH} -ENV LC_ALL "${LOCALIZATION}" - -WORKDIR /home/${CONTAINER_USER}/workspace - -CMD ["zsh"] +# Provide APP_START_CMD via --env-file. +CMD ["bash", "scripts/build.sh"] diff --git a/EXAMPLES.md b/EXAMPLES.md deleted file mode 100644 index 307e39e..0000000 --- a/EXAMPLES.md +++ /dev/null @@ -1,114 +0,0 @@ -# Approov Integrations Examples - -[Approov](https://approov.io) is an API security solution used to verify that requests received by your backend services originate from trusted versions of your mobile apps, and here you can find the Hello servers examples that are the base for the Approov [quickstarts](/docs) for NodeJS. - -For more information about how Approov works and why you should use it you can read the [Approov Overview](/OVERVIEW.md) at the root of this repo. - -If you are looking for the Approov quickstarts to integrate Approov in your NodeJS API server then you can find them [here](/QUICKSTARTS.md). - - -## Hello Server Examples - -To learn more about each Hello server example you need to read the README for each one at: - -* [Unprotected Server](./src/unprotected-server) -* [Approov Protected Server - Token Check](./src/approov-protected-server/token-check) -* [Approov Protected Server - Token Binding Check](./src/approov-protected-server/token-binding-check) - - -## Docker Stack - -The docker stack provided via the `docker-compose.yml` file in this folder is used for development proposes and if you are familiar with docker then feel free to also use it to follow along the examples on the README of each server. - -If you decide to use the docker stack then you need to bear in mind that the Postman collections, used to test the servers examples, will connect to port `8002` therefore you cannot start all docker compose services at once, for example with `docker-compose up`, instead you need to run one at a time as exemplified below. - -### Setup Env File - -Do not forget to properly setup the `.env` file in the root of each Approov protected server example before you run the server with the docker stack. - -```bash -cp src/unprotected-server/.env.example src/unprotected-server/.env -cp src/approov-protected-server/token-check/.env.example src/approov-protected-server/token-check/.env -cp src/approov-protected-server/token-binding-check/.env.example src/approov-protected-server/token-binding-check/.env -``` - -Edit each file and add the [dummy secret](/TESTING.md#the-dummy-secret) to it in order to be able to test the Approov integration with the provided [Postman collection](https://github.com/approov/postman-collections/blob/master/quickstarts/hello-world/hello-world.postman_curl_requests_examples.md). - - -### Build the Docker Stack - -The three services in the `docker-compose.yml` use the same Dockerfile, therefore to build the Docker image we just need to used one of them: - -```bash -sudo docker-compose build approov-token-binding-check -``` - -Now, you are ready to start using the Docker stack for NodeJS. - - -### Command Examples - -To run each of the Hello servers with docker compose you just need to follow the respective example below. - -#### For the unprotected server - -Run the container attached to your machine bash shell: - -```bash -sudo docker-compose up unprotected-server -``` - -or get a bash shell inside the container: - -```bash -sudo docker-compose run --rm --service-ports unprotected-server zsh -``` - -#### For the Approov Token Check - -Run the container attached to the shell: - -```bash -sudo docker-compose up approov-token-check -``` - -or get a bash shell inside the container: - -```bash -sudo docker-compose run --rm --service-ports approov-token-check zsh -``` - -#### For the Approov Token Binding Check - -Run the container attached to the shell: - -```bash -sudo docker-compose up approov-token-binding-check -``` - -or get a bash shell inside the container: - -```bash -sudo docker-compose run --rm --service-ports approov-token-binding-check zsh -``` - - -## Issues - -If you find any issue while following the example then just open an issue on this repo with the steps to reproduce it and we will help you to solve them. - - -## Useful Links - -If you wish to explore the Approov solution in more depth, then why not try one of the following links as a jumping off point: - -* [Approov Free Trial](https://approov.io/signup)(no credit card needed) -* [Approov Get Started](https://approov.io/product/demo) -* [Approov QuickStarts](https://approov.io/docs/latest/approov-integration-examples/) -* [Approov Docs](https://approov.io/docs) -* [Approov Blog](https://approov.io/blog/) -* [Approov Resources](https://approov.io/resource/) -* [Approov Customer Stories](https://approov.io/customer) -* [Approov Support](https://approov.io/contact) -* [About Us](https://approov.io/company) -* [Contact Us](https://approov.io/contact) diff --git a/OVERVIEW.md b/OVERVIEW.md deleted file mode 100644 index 40f2398..0000000 --- a/OVERVIEW.md +++ /dev/null @@ -1,55 +0,0 @@ -# Approov Overview - -[Approov](https://approov.io) is an API security solution used to verify that requests received by your backend services originate from trusted versions of your mobile apps. - - -## Why? - -You can learn more about Approov, the motives for adopting it, and more detail on how it works by following this [link](https://approov.io/product). In brief, Approov: - -* Ensures that accesses to your API come from official versions of your apps; it blocks accesses from republished, modified, or tampered versions -* Protects the sensitive data behind your API; it prevents direct API abuse from bots or scripts scraping data and other malicious activity -* Secures the communication channel between your app and your API with [Approov Dynamic Certificate Pinning](https://approov.io/docs/latest/approov-usage-documentation/#approov-dynamic-pinning). This has all the benefits of traditional pinning but without the drawbacks -* Removes the need for an API key in the mobile app -* Provides DoS protection against targeted attacks that aim to exhaust the API server resources to prevent real users from reaching the service or to at least degrade the user experience. - - -## How it works? - -This is a brief overview of how the Approov cloud service and the backend server fit together from a backend perspective. For a complete overview of how the mobile app and backend fit together with the Approov cloud service and the Approov SDK we recommend to read the [Approov overview](https://approov.io/product) page on our website. - -### Approov Cloud Service - -The Approov cloud service attests that a device is running a legitimate and tamper-free version of your mobile app. - -* If the integrity check passes then a valid token is returned to the mobile app -* If the integrity check fails then a legitimate looking token will be returned - -In either case, the app, unaware of the token's validity, adds it to every request it makes to the Approov protected API(s). - -### The Backend Server - -The backend server ensures that the token supplied in the `Approov-Token` header is present and valid. The validation is done by using a shared secret known only to the Approov cloud service and the backend server. - -The request is handled such that: - -* If the Approov Token is valid, the request is allowed to be processed by the API endpoint -* If the Approov Token is invalid, an HTTP 401 Unauthorized response is returned - -You can choose to log JWT verification failures, but we left it out on purpose so that you can have the choice of how you prefer to do it and decide the right amount of information you want to log. - - -## Useful Links - -If you wish to explore the Approov solution in more depth, then why not try one of the following links as a jumping off point: - -* [Approov Free Trial](https://approov.io/signup)(no credit card needed) -* [Approov Get Started](https://approov.io/product/demo) -* [Approov QuickStarts](https://approov.io/docs/latest/approov-integration-examples/) -* [Approov Docs](https://approov.io/docs) -* [Approov Blog](https://approov.io/blog/) -* [Approov Resources](https://approov.io/resource/) -* [Approov Customer Stories](https://approov.io/customer) -* [Approov Support](https://approov.io/contact) -* [About Us](https://approov.io/company) -* [Contact Us](https://approov.io/contact) diff --git a/QUICKSTARTS.md b/QUICKSTARTS.md deleted file mode 100644 index 37b80e3..0000000 --- a/QUICKSTARTS.md +++ /dev/null @@ -1,51 +0,0 @@ -# Approov Integration Quickstarts - -[Approov](https://approov.io) is an API security solution used to verify that requests received by your backend services originate from trusted versions of your mobile apps. - - -## The Quickstarts - -The quickstart code for the Approov backend server is split into two implementations. The first gets you up and running with basic token checking. The second uses a more advanced Approov feature, _token binding_. Token binding may be used to link the Approov token with other properties of the request, such as user authentication (more details can be found [here](https://approov.io/docs/latest/approov-usage-documentation/#token-binding)). -* [Approov token check quickstart](/docs/APPROOV_TOKEN_QUICKSTART.md) -* [Approov token check with token binding quickstart](/docs/APPROOV_TOKEN_BINDING_QUICKSTART.md) - -Both the quickstarts are built from the unprotected example server defined [here](/src/unprotected-server/hello-server-unprotected.js), thus you can use Git to see the code differences between them. - -Code difference between the Approov token check quickstart and the original unprotected server: - -``` -git diff --no-index src/unprotected-server/hello-server-unprotected.js src/approov-protected-server/token-check/hello-server-protected.js -``` - -You can do the same for the Approov token binding quickstart: - -``` -git diff --no-index src/unprotected-server/hello-server-unprotected.js src/approov-protected-server/token-binding-check/hello-server-protected.js -``` - -Or you can compare the code difference between the two quickstarts: - -``` -git diff --no-index src/approov-protected-server/token-check/hello-server-protected.js src/approov-protected-server/token-binding-check/hello-server-protected.js -``` - - -## Issues - -If you find any issue while following our instructions then just report it [here](https://github.com/approov/quickstart-nodejs-token-check/issues), with the steps to reproduce it, and we will sort it out and/or guide you to the correct path. - - -## Useful Links - -If you wish to explore the Approov solution in more depth, then why not try one of the following links as a jumping off point: - -* [Approov Free Trial](https://approov.io/signup)(no credit card needed) -* [Approov Get Started](https://approov.io/product/demo) -* [Approov QuickStarts](https://approov.io/docs/latest/approov-integration-examples/) -* [Approov Docs](https://approov.io/docs) -* [Approov Blog](https://approov.io/blog/) -* [Approov Resources](https://approov.io/resource/) -* [Approov Customer Stories](https://approov.io/customer) -* [Approov Support](https://approov.io/contact) -* [About Us](https://approov.io/company) -* [Contact Us](https://approov.io/contact) diff --git a/README.md b/README.md index 1701d6d..67d8295 100644 --- a/README.md +++ b/README.md @@ -1,147 +1,247 @@ -# Approov QuickStart - NodeJS Token Check +# Approov Backend Quickstart - Node.js -[Approov](https://approov.io) is an API security solution used to verify that requests received by your backend services originate from trusted versions of your mobile apps. +This project provides a server-side example of Approov token verification for a protected backend API. It exposes a simple API that verifies Approov tokens before granting access to protected endpoints and demonstrates how the endpoints behave under the current Approov configuration: -This repo implements the Approov server-side request verification code in NodeJS (framework agnostic), which performs the verification check before allowing valid traffic to be processed by the API endpoint. + - `/unprotected` - no Approov token required. + - `/token-check` - requires a valid Approov token. + - `/token-binding` - requires a valid Approov token which is bound to a header value. + - `/token-double-binding` - requires a valid Approov token which is bound to two header values. +In this example, Approov protection is enforced by `approovTokenVerifier` (see [ApproovApplication.js](ApproovApplication.js#L199-L235)), which reads the `Approov-Token`, verifies its signature/exp, and validates token bindings before returning `401` on failure. Protected endpoints are the routes listed in `ROUTES` (see [ApproovApplication.js](ApproovApplication.js#L32-L88)), which are only accessible when this middleware passes. -## Approov Integration Quickstart +Implementation pointers (with line ranges): +- Middleware/filter implementation: [ApproovApplication.js](ApproovApplication.js#L199-L235) +- Protected route configuration: [ApproovApplication.js](ApproovApplication.js#L32-L88) +- Token/binding validation logic (`verifyApproovToken`, `extractBindingValue`, `isBindingValid`): [ApproovApplication.js](ApproovApplication.js#L237-L303). -The quickstart was tested with the following Operating Systems: +## Approov Token Verification Flow -* Ubuntu 20.04 -* MacOS Big Sur -* Windows 10 WSL2 - Ubuntu 20.04 +1. **Token Request:** + The Approov SDK inside the mobile app securely communicates with the Approov Cloud Service to obtain a short-lived [Approov Token](https://ext.approov.io/docs/latest/approov-usage-documentation/#approov-tokens) (a signed JWT). + Additionally, you can use the CLI [token commands](https://ext.approov.io/docs/latest/approov-cli-tool-reference/#token-commands) to validate tokens, generate new ones, and set the data hash. -First, setup the [Approov CLI](https://approov.io/docs/latest/approov-installation/index.html#initializing-the-approov-cli). +2. **Token Attachment:** + The app attaches this token to every API request using the `Approov-Token` HTTP header. -Now, register the API domain for which Approov will issues tokens: +3. **Server Validation:** + The [server verifies](https://ext.approov.io/docs/latest/approov-usage-documentation/#approov-architecture) the token using the shared Approov secret, checking its: + - Signature authenticity + - Expiration (`exp` claim) + - Other claims if configured + +4. **Token Binding (Optional):** + [Token binding](https://ext.approov.io/docs/latest/approov-usage-documentation/#token-binding) is configured by the app via the Approov SDK, which hashes a chosen binding value (for example the `Authorization` header) and embeds it into the Approov token. + The protected API then computes the same hash from the incoming request and verifies that it matches the `pay` claim, preventing token reuse or replay attacks. For local testing, you can also generate example tokens with a binding using the Approov CLI. + +5. **Request Decision:** + If all checks pass → the request is trusted and processed `200 OK`. + If validation fails → the server responds with `401 Unauthorized`. + +## Requirements: + +1. ***Approov account*** - If you're new, sign up for an [Approov trial account](https://approov.io/signup). +2. ***Approov CLI initialized*** - Follow the [installation guide](https://ext.approov.io/docs/latest/approov-installation/#initializing-the-approov-cli) and confirm `approov whoami` works. +3. ***Install curl*** - Ensure the `curl` CLI is available. +4. ***Create .env file*** - copy `.env.example` so there is a place to store the secret key. + ```bash + cp .env.example .env + ``` + +5. ***Configure secret*** - fetch the secret and add it to `.env` (`APPROOV_BASE64URL_SECRET`): + ```bash + approov secret -get base64url + ``` + +6. ***Register API domain*** - point Approov at your backend API (default example.com): + ```bash + approov api -add example.com + ``` + +7. ***Install Docker and Docker Compose*** - follow the official guide: [Docker docs](https://docs.docker.com/get-started/get-docker/) + +## Try it yourself using Docker + +*If you have all requirements, you can run* ```bash -approov api -add api.example.com +bash run-server.sh ``` -> **NOTE:** By default a symmetric key (HS256) is used to sign the Approov token on a valid attestation of the mobile app for each API domain it's added with the Approov CLI, so that all APIs will share the same secret and the backend needs to take care to keep this secret secure. -> -> A more secure alternative is to use asymmetric keys (RS256 or others) that allows for a different keyset to be used on each API domain and for the Approov token to be verified with a public key that can only verify, but not sign, Approov tokens. -> -> To implement the asymmetric key you need to change from using the symmetric HS256 algorithm to an asymmetric algorithm, for example RS256, that requires you to first [add a new key](https://approov.io/docs/latest/approov-usage-documentation/#adding-a-new-key), and then specify it when [adding each API domain](https://approov.io/docs/latest/approov-usage-documentation/#keyset-key-api-addition). Please visit [Managing Key Sets](https://approov.io/docs/latest/approov-usage-documentation/#managing-key-sets) on the Approov documentation for more details. +This script: +- Builds and starts the container via `scripts/build.sh` (`docker build` + `docker run`) and waits for `/approov-state` to be ready. + +*Once finished, press `Ctrl+C` to stop log tailing; the container keeps running unless you stop it. Use `docker ps` to find the container name and `docker stop ` to stop it.* -Next, enable your Approov `admin` role with: +### Automated and Manual Testing + +*When the server is running (in a different terminal), validate the endpoints via the automated bash script or by running the manual checks below* ```bash -eval `approov role admin` -```` +bash test.sh +``` + +This script: +- Verifies that the `approov` and `curl` commands are installed. +- Checks Approov status by calling `/approov-state` (enabled vs disabled). +- Runs endpoint tests against `/unprotected` (no token), `/token-check` (valid/invalid Approov tokens), `/token-binding` (token bound to `Authorization`), and `/token-double-binding` (token bound to `Authorization` + `Content-Digest`). +- Logs full request/response details to `.config/logs/.log`. + +#### *1. Unprotected Endpoint (No Approov)* + +- The client sends a normal HTTP request. +- The server **does not verify** any Approov token or extra authentication header. +- This means **any client** (even tampered or unauthorized) can call the API if they know the URL. + +*The following example shows how the API responds when no Approov protection is applied.* + +```bash +curl -iX GET http://localhost:8080/unprotected +``` -For the Windows powershell: +The response will be `200 OK` for this request: +```text +HTTP/1.1 200 OK +Content-Type: application/json +Cache-Control: no-cache +``` + +#### *2. Approov Token Check* + +- The client includes an `Approov-Token` (a short-lived JWT) in each API request header. +- The server verifies this token using the **Approov secret key** that is securely configured on the backend and checks: + - Token verification - confirms the token is signed by the Approov secret. + - Expiration (`exp` claim) - ensures the token is still valid. +- If the token is valid → request is trusted. +- If invalid → server returns `401 Unauthorized`. +- **Purpose**: Protect API endpoints so that only authentic, unmodified Approov-integrated apps can access them. + +***The following example shows how the API responds when an Approov token is required.*** + +*Generate a valid Approov token:* ```bash -set APPROOV_ROLE=admin:___YOUR_APPROOV_ACCOUNT_NAME_HERE___ -```` +approov token -genExample example.com +``` -Now, get your Approov Secret with the [Approov CLI](https://approov.io/docs/latest/approov-installation/index.html#initializing-the-approov-cli): +*Use the generated token in the `Approov-Token` header and `/token-check` endpoint.* ```bash -approov secret -get base64 +curl -iX GET http://localhost:8080/token-check \ + -H "Approov-Token: valid_approov_token_here" ``` -Next, add the [Approov secret](https://approov.io/docs/latest/approov-usage-documentation/#account-secret-key-export) to your project `.env` file: +The response will be `200 OK` for this request: -```env -APPROOV_BASE64_SECRET=approov_base64_secret_here +```text +HTTP/1.1 200 OK +Content-Type: application/json +Cache-Control: no-cache ``` -Now, add to your `package.json` file the [JWT dependency](https://github.com/auth0/node-jsonwebtoken#readme): +*If you use an invalid or missing token, the server will respond with `401 Unauthorized`.* + +#### *3. Approov Token Binding Check* + +- The client sends two headers on authenticated API calls: + - `Approov-Token` + - `Authorization` – your auth token value (e.g., `ExampleAuthToken==`) +- The server verifies the token and ensures that the bound value matches what the app used. +- Prevents token replay - the Approov token cannot be reused or stolen for another session. +- **Use case:** Stronger protection for authenticated API calls tied to a specific user or device. + +***The following example shows how the API responds when an Approov token with binding is required.*** + +*Generate a valid Approov token bound to the `Authorization` header:* -```json -"jsonwebtoken": "^8.5.1" +```bash +approov token -setDataHashInToken ExampleAuthToken== -genExample example.com ``` -Next, in your code require the [JWT dependency](https://github.com/auth0/node-jsonwebtoken#readme): +*Use the generated token with binding in the Approov-Token and Authorization headers when calling the /token-binding endpoint.* -```javascript -const jwt = require('jsonwebtoken') +```bash +curl -iX GET http://localhost:8080/token-binding \ + -H "Approov-Token: valid_approov_token_here" \ + -H "Authorization: ExampleAuthToken==" ``` -Now, grab the Approov secret into a constant: +The response will be `200 OK` for this request: -```javascript -const dotenv = require('dotenv').config() -const approovBase64Secret = dotenv.parsed.APPROOV_BASE64_SECRET; -const approovSecret = Buffer.from(approovBase64Secret, 'base64') +```text +HTTP/1.1 200 OK +Content-Type: application/json +Cache-Control: no-cache ``` -Next, verify the Approov token: +*If you use an invalid or missing header or token, the server will respond with `401 Unauthorized`.* -```javascript -const verifyApproovToken = function(req) { +#### Approov Token Binding Check with Two Different Bound Values - const approovToken = req.headers['approov-token'] +- The client sends three headers on authenticated API calls: + - `Approov-Token` + - `Authorization` + - `Content-Digest` It is combined with the `Authorization` header to create a stronger binding. +- Both are included in the hash inside the Approov token. This means the server verifies a single hash that covers both authentication credentials. +- **Use case:** Stronger protection then single binding by tying both headers together. - if (!approovToken) { - // You may want to add some logging here. - return false - } +***The following example shows how the API responds when an Approov token with two bindings is required.*** - // Decode the token with strict verification of the signature (['HS256']) to - // prevent against the `none` algorithm attack. - return jwt.verify(approovToken, approovSecret, { algorithms: ['HS256'] }, function(err, decoded) { +*Generate a valid Approov token bound to the `Authorization` and `Content-Digest` headers:* - if (err) { - // You may want to add some logging here. - return false - } +```bash +approov token -setDataHashInToken ExampleAuthToken==ContentDigest== -genExample example.com +``` - // The Approov token was successfully verified. We will add the claims to the - // request object to allow further use of them during the request processing. - req.approovTokenClaims = decoded +*Use the generated token with two bindings in the Approov-Token and Authorization headers when calling the `/token-double-binding` endpoint.* - return true - }) -} +```bash +curl -iX GET http://localhost:8080/token-double-binding \ + -H "Approov-Token: valid_approov_token_here" \ + -H "Authorization: ExampleAuthToken==" \ + -H "Content-Digest: ContentDigest==" ``` -Finally, invoke the check for each API endpoint you want to protect with Approov: +The response will be `200 OK` for this request. -```javascript -if (!verifyApproovToken(req)) { - res.statusCode = 401 - res.end(JSON.stringify({})) - return -} +```text +HTTP/1.1 200 OK +Content-Type: application/json +Cache-Control: no-cache ``` -Not enough details in the bare bones quickstart? No worries, check the [detailed quickstarts](QUICKSTARTS.md) that contain a more comprehensive set of instructions, including how to test the Approov integration. +*If you use an invalid or missing header or token, the server will respond with `401 Unauthorized`.* +## Enable or Disable Approov Protection -## More Information +When the example server is running on `localhost:8080`, you can toggle Approov protection with these commands: -* [Approov Overview](OVERVIEW.md) -* [Detailed Quickstarts](QUICKSTARTS.md) -* [Examples](EXAMPLES.md) -* [Testing](TESTING.md) +```bash +curl -X POST http://localhost:8080/approov/disable # disable the Approov service -### System Clock +curl -X POST http://localhost:8080/approov/enable # enable the Approov service -In order to correctly check for the expiration times of the Approov tokens is very important that the backend server is synchronizing automatically the system clock over the network with an authoritative time source. In Linux this is usually done with a NTP server. +curl -X GET http://localhost:8080/approov-state # check current state +``` +*You can rerun the tests with Approov disabled to observe how the application behaves when the Approov protection is ***no longer active***.* -## Issues +## Reporting Issues -If you find any issue while following our instructions then just report it [here](https://github.com/approov/quickstart-nodejs-token-check/issues), with the steps to reproduce it, and we will sort it out and/or guide you to the correct path. +**Environments where the quickstart was tested:** +```text +* Runtime: node.js v25.2.1 +* Build Tool: npm 11.6.2 +``` +If you encounter any problems while following this guide, or have any other concerns, please let us know by opening an issue [here](https://github.com/approov/quickstart-nodejs-token-check/issues) and we will be happy to assist you. ## Useful Links -If you wish to explore the Approov solution in more depth, then why not try one of the following links as a jumping off point: - -* [Approov Free Trial](https://approov.io/signup)(no credit card needed) -* [Approov Get Started](https://approov.io/product/demo) -* [Approov QuickStarts](https://approov.io/docs/latest/approov-integration-examples/) -* [Approov Docs](https://approov.io/docs) -* [Approov Blog](https://approov.io/blog/) +* [Approov QuickStarts](https://approov.io/resource/quickstarts/) +* [Approov Docs](https://ext.approov.io/docs) +* [Approov Blog](https://approov.io/blog) * [Approov Resources](https://approov.io/resource/) * [Approov Customer Stories](https://approov.io/customer) -* [Approov Support](https://approov.io/contact) +* [Approov Support](https://approov.io/info/technical-support) * [About Us](https://approov.io/company) -* [Contact Us](https://approov.io/contact) +* [Contact Us](https://approov.io/info/contact) \ No newline at end of file diff --git a/TESTING.md b/TESTING.md deleted file mode 100644 index 5a0dfb8..0000000 --- a/TESTING.md +++ /dev/null @@ -1,43 +0,0 @@ -# Approov Integration Testing - -[Approov](https://approov.io) is an API security solution used to verify that requests received by your backend services originate from trusted versions of your mobile apps. - -## Testing the Approov Integration - -Each Quickstart has at their end a dedicated section for testing, that will walk you through the necessary steps to use the Approov CLI to generate valid and invalid tokens to test your Approov integration without the need to rely on the genuine mobile app(s) using your backend. - -* [Approov Token](/docs/APPROOV_TOKEN_QUICKSTART.md#test-your-approov-integration) test examples. -* [Approov Token Binding](/docs/APPROOV_TOKEN_BINDING_QUICKSTART.md#test-your-approov-integration) test examples. - -### Testing with Postman - -A ready-to-use Postman collection can be found [here](https://raw.githubusercontent.com/approov/postman-collections/master/quickstarts/hello-world/hello-world.postman_collection.json). It contains a comprehensive set of example requests to send to the backend server for testing. The collection contains requests with valid and invalid Approov tokens, and with and without token binding. - -### Testing with Curl - -An alternative to the Postman collection is to use cURL to make the API requests. Check some examples [here](https://github.com/approov/postman-collections/blob/master/quickstarts/hello-world/hello-world.postman_curl_requests_examples.md). - -### The Dummy Secret - -The valid Approov tokens in the Postman collection and cURL requests examples were signed with a dummy secret that was generated with `openssl rand -base64 64 | tr -d '\n'; echo`, therefore not a production secret retrieved with `approov secret -get base64`, thus in order to use it you need to set the `APPROOV_BASE64_SECRET`, in the `.env` file for each [Approov integration example](/src/approov-protected-server), to the following value: `h+CX0tOzdAAR9l15bWAqvq7w9olk66daIH+Xk+IAHhVVHszjDzeGobzNnqyRze3lw/WVyWrc2gZfh3XXfBOmww==`. - - -## Issues - -If you find any issue while following our instructions then just report it [here](https://github.com/approov/quickstart-nodejs-token-check/issues), with the steps to reproduce it, and we will sort it out and/or guide you to the correct path. - - -## Useful Links - -If you wish to explore the Approov solution in more depth, then why not try one of the following links as a jumping off point: - -* [Approov Free Trial](https://approov.io/signup)(no credit card needed) -* [Approov Get Started](https://approov.io/product/demo) -* [Approov QuickStarts](https://approov.io/docs/latest/approov-integration-examples/) -* [Approov Docs](https://approov.io/docs) -* [Approov Blog](https://approov.io/blog/) -* [Approov Resources](https://approov.io/resource/) -* [Approov Customer Stories](https://approov.io/customer) -* [Approov Support](https://approov.io/contact) -* [About Us](https://approov.io/company) -* [Contact Us](https://approov.io/contact) diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index bf73e97..0000000 --- a/docker-compose.yml +++ /dev/null @@ -1,37 +0,0 @@ -version: "2.3" - -services: - - unprotected-server: - image: approov/nodejs:17.7.1 - build: ./ - networks: - - default - command: sh -c "npm install && npm start" - ports: - - ${HOST_IP:-127.0.0.1}:${HTTP_PORT:-8002}:${HTTP_PORT:-8002} - volumes: - - ./src/unprotected-server:/home/node/workspace - - approov-token-check: - image: approov/nodejs:17.7.1 - build: ./ - networks: - - default - command: sh -c "npm install && npm start" - ports: - - ${HOST_IP:-127.0.0.1}:${HTTP_PORT:-8002}:${HTTP_PORT:-8002} - volumes: - - ./src/approov-protected-server/token-check:/home/node/workspace - - approov-token-binding-check: - image: approov/nodejs:17.7.1 - build: ./ - networks: - - default - command: sh -c "npm install && npm start" - ports: - - ${HOST_IP:-127.0.0.1}:${HTTP_PORT:-8002}:${HTTP_PORT:-8002} - volumes: - - ./src/approov-protected-server/token-binding-check:/home/node/workspace - diff --git a/docs/APPROOV_TOKEN_BINDING_QUICKSTART.md b/docs/APPROOV_TOKEN_BINDING_QUICKSTART.md deleted file mode 100644 index 5e10c48..0000000 --- a/docs/APPROOV_TOKEN_BINDING_QUICKSTART.md +++ /dev/null @@ -1,243 +0,0 @@ -# Approov Token Binding Quickstart - -This quickstart is for developers familiar with NodeJS who are looking for a quick intro into how they can add [Approov](https://approov.io) into an existing project. Therefore this will guide you through the necessary steps for adding Approov with token binding to an existing NodeJS API server. - -## TOC - Table of Contents - -* [Why?](#why) -* [How it Works?](#how-it-works) -* [Requirements](#requirements) -* [Approov Setup](#approov-setup) -* [Approov Token Check](#approov-token-binding-check) -* [Test the Approov Integration](#test-your-approov-integration) - - -## Why? - -To lock down your API server to your mobile app. Please read the brief summary in the [Approov Overview](/OVERVIEW.md#why) at the root of this repo or visit our [website](https://approov.io/product/) for more details. - -[TOC](#toc---table-of-contents) - - -## How it works? - -For more background, see the [Approov Overview](/OVERVIEW.md#how-it-works) at the root of this repository. - -The main functionality for the Approov token binding check is in the file [src/approov-protected-server/token-binding-check/hello-server-protected.js](/src/approov-protected-server/token-binding-check/hello-server-protected.js). Take a look at the `verifyApproovToken()` and `verifyApproovTokenBinding()` functions to see the simple code for the checks. - -[TOC](#toc---table-of-contents) - - -## Requirements - -To complete this quickstart you will need both the NodeJS and the Approov CLI tool installed. - -* NodeJS - Follow the official installation instructions from [here](https://nodejs.org/en/download/) -* Approov CLI - Follow our [installation instructions](https://approov.io/docs/latest/approov-installation/#approov-tool) and read more about each command and its options in the [documentation reference](https://approov.io/docs/latest/approov-cli-tool-reference/) - -[TOC](#toc---table-of-contents) - - -## Approov Setup - -To use Approov with the NodeJS API server we need a small amount of configuration. First, Approov needs to know the API domain that will be protected. Second, the NodeJS API server needs the Approov Base64 encoded secret that will be used to verify the tokens generated by the Approov cloud service. - -### Configure API Domain - -Approov needs to know the domain name of the API for which it will issue tokens. - -Add it with: - -```bash -approov api -add your.api.domain.com -``` - -> **NOTE:** By default a symmetric key (HS256) is used to sign the Approov token on a valid attestation of the mobile app for each API domain it's added with the Approov CLI, so that all APIs will share the same secret and the backend needs to take care to keep this secret secure. -> -> A more secure alternative is to use asymmetric keys (RS256 or others) that allows for a different keyset to be used on each API domain and for the Approov token to be verified with a public key that can only verify, but not sign, Approov tokens. -> -> To implement the asymmetric key you need to change from using the symmetric HS256 algorithm to an asymmetric algorithm, for example RS256, that requires you to first [add a new key](https://approov.io/docs/latest/approov-usage-documentation/#adding-a-new-key), and then specify it when [adding each API domain](https://approov.io/docs/latest/approov-usage-documentation/#keyset-key-api-addition). Please visit [Managing Key Sets](https://approov.io/docs/latest/approov-usage-documentation/#managing-key-sets) on the Approov documentation for more details. - -Adding the API domain also configures the [dynamic certificate pinning](https://approov.io/docs/latest/approov-usage-documentation/#dynamic-pinning) setup, out of the box. - -> **NOTE:** By default the pin is extracted from the public key of the leaf certificate served by the domain, as visible to the box issuing the Approov CLI command and the Approov servers. - -### Approov Secret - -Approov tokens are signed with a symmetric secret. To verify tokens, we need to grab the secret using the [Approov secret command](https://approov.io/docs/latest/approov-cli-tool-reference/#secret-command) and plug it into the NodeJS API server environment to check the signatures of the [Approov Tokens](https://www.approov.io/docs/latest/approov-usage-documentation/#approov-tokens) that it processes. - -Retrieve the Approov secret with: - -```bash -approov secret -get base64 -``` - -> **NOTE:** The `approov secret` command requires an [administration role](https://approov.io/docs/latest/approov-usage-documentation/#account-access-roles) to execute successfully. - -#### Set the Approov Secret - -Open the `.env` file and add the Approov secret to the var: - -```bash -APPROOV_BASE64_SECRET=approov_base64_secret_here -``` - -[TOC](#toc---table-of-contents) - - -## Approov Token Check - -To check the Approov token we will use the [auth0/node-jsonwebtoken](https://github.com/auth0/node-jsonwebtoken#readme) package, but you are free to use another one of your preference. - -Add this functions to your code: - -```javascript -const verifyApproovToken = function(req) { - - const approovToken = req.headers['approov-token'] - - if (!approovToken) { - // You may want to add some logging here. - return false - } - - // decode token, verify secret and check exp - return jwt.verify(approovToken, approovSecret, { algorithms: ['HS256'] }, function(err, decoded) { - - if (err) { - // You may want to add some logging here. - return false - } - - // The Approov token was successfully verified. We will add the claims to - // the request object to allow further use of them during the request - // processing. - req.approovTokenClaims = decoded - - return true - }) -} - -const verifyApproovTokenBinding = function(req) { - - if (!("pay" in req.approovTokenClaims)) { - // You may want to add some logging here. - return false - } - - // The Approov token claims is added to the request object on a successful - // Approov token verification. See `verifyApproovToken()` function. - token_binding_claim = req.approovTokenClaims.pay - - // 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. - token_binding_header = req.headers['authorization'] - - if (!token_binding_header) { - // You may want to add some logging here. - return false - } - - // We need to hash and base64 encode the token binding header, because thats - // 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') - - return token_binding_claim === token_binding_header_encoded -} -``` - -Now you just need to invoke it from the endpoint you want to protected: - -```javascript -if (!verifyApproovToken(req)) { - res.statusCode = 401 - res.end(JSON.stringify({})) - return - } - - if (!verifyApproovTokenBinding(req)) { - res.statusCode = 401 - res.end(JSON.stringify({})) - return - } -``` - -> **NOTE:** When the Approov token validation fails we return a `401` with an empty body, because we don't want to give clues to an attacker about the reason the request failed, and you can go even further by returning a `400`. - -A full working example for a simple Hello World server can be found at [src/approov-protected-server/token-binding-check](/src/approov-protected-server/token-binding-check). - -[TOC](#toc---table-of-contents) - - -## Test your Approov Integration - -The following examples below use cURL, but you can also use the [Postman Collection](/README.md#testing-with-postman) to make the API requests. Just remember that you need to adjust the urls and tokens defined in the collection to match your deployment. Alternatively, the above README also contains instructions for using the preset _dummy_ secret to test your Approov integration. - -#### With Valid Approov Tokens - -Generate a valid token example from the Approov Cloud service: - -```bash -approov token -setDataHashInToken 'Bearer authorizationtoken' -genExample your.api.domain.com -``` - -Then make the request with the generated token: - -```bash -curl -i --request GET 'https://your.api.domain.com/v1/shapes' \ - --header 'Authorization: Bearer authorizationtoken' \ - --header 'Approov-Token: APPROOV_TOKEN_EXAMPLE_HERE' -``` - -The request should be accepted. For example: - -```text -HTTP/2 200 - -... - -{"message": "Hello, World!"} -``` - -#### With Invalid Approov Tokens - -##### No Authorization Token - -Let's just remove the Authorization header from the request: - -```bash -curl -i --request GET 'https://your.api.domain.com/v1/shapes' \ - --header 'Approov-Token: APPROOV_TOKEN_EXAMPLE_HERE' -``` - -The above request should fail with an Unauthorized error. For example: - -```text -HTTP/2 401 - -... - -{} -``` - -##### Same Approov Token with a Different Authorization Token - -Make the request with the same generated token, but with another random authorization token: - -```bash -curl -i --request GET 'https://your.api.domain.com/v1/shapes' \ - --header 'Authorization: Bearer anotherauthorizationtoken' \ - --header 'Approov-Token: APPROOV_TOKEN_EXAMPLE_HERE' -``` - -The above request should also fail with an Unauthorized error. For example: - -```text -HTTP/2 401 - -... - -{} -``` - -[TOC](#toc---table-of-contents) diff --git a/docs/APPROOV_TOKEN_QUICKSTART.md b/docs/APPROOV_TOKEN_QUICKSTART.md deleted file mode 100644 index bc2439d..0000000 --- a/docs/APPROOV_TOKEN_QUICKSTART.md +++ /dev/null @@ -1,193 +0,0 @@ -# Approov Token Quickstart - -This quickstart is for developers familiar with NodeJS who are looking for a quick intro into how they can add [Approov](https://approov.io) into an existing project. Therefore this will guide you through the necessary steps for adding Approov to an existing NodeJS API server. - -## TOC - Table of Contents - -* [Why?](#why) -* [How it Works?](#how-it-works) -* [Requirements](#requirements) -* [Approov Setup](#approov-setup) -* [Approov Token Check](#approov-token-check) -* [Test the Approov Integration](#test-your-approov-integration) - - -## Why? - -To lock down your API server to your mobile app. Please read the brief summary in the [Approov Overview](/OVERVIEW.md#why) at the root of this repo or visit our [website](https://approov.io/product/) for more details. - -[TOC](#toc---table-of-contents) - - -## How it works? - -For more background, see the [Approov Overview](/OVERVIEW.md#how-it-works) at the root of this repo. - -The main functionality for the Aroov token check is in the file [src/approov-protected-server/token-check/hello-server-protected.js](/src/approov-protected-server/token-check/hello-server-protected.js). Take a look at the `verifyApproovToken()` function to see the simple code for the check. - -[TOC](#toc---table-of-contents) - - -## Requirements - -To complete this quickstart you will need both the NodeJS and the Approov CLI tool installed. - -* NodeJS - Follow the official installation instructions from [here](https://nodejs.org/en/download/) -* Approov CLI - Follow our [installation instructions](https://approov.io/docs/latest/approov-installation/#approov-tool) and read more about each command and its options in the [documentation reference](https://approov.io/docs/latest/approov-cli-tool-reference/) - -[TOC](#toc---table-of-contents) - - -## Approov Setup - -To use Approov with the NodeJS API server we need a small amount of configuration. First, Approov needs to know the API domain that will be protected. Second, the NodeJS API server needs the Approov Base64 encoded secret that will be used to verify the tokens generated by the Approov cloud service. - -### Configure API Domain - -Approov needs to know the domain name of the API for which it will issue tokens. - -Add it with: - -```bash -approov api -add your.api.domain.com -``` - -> **NOTE:** By default a symmetric key (HS256) is used to sign the Approov token on a valid attestation of the mobile app for each API domain it's added with the Approov CLI, so that all APIs will share the same secret and the backend needs to take care to keep this secret secure. -> -> A more secure alternative is to use asymmetric keys (RS256 or others) that allows for a different keyset to be used on each API domain and for the Approov token to be verified with a public key that can only verify, but not sign, Approov tokens. -> -> To implement the asymmetric key you need to change from using the symmetric HS256 algorithm to an asymmetric algorithm, for example RS256, that requires you to first [add a new key](https://approov.io/docs/latest/approov-usage-documentation/#adding-a-new-key), and then specify it when [adding each API domain](https://approov.io/docs/latest/approov-usage-documentation/#keyset-key-api-addition). Please visit [Managing Key Sets](https://approov.io/docs/latest/approov-usage-documentation/#managing-key-sets) on the Approov documentation for more details. - -Adding the API domain also configures the [dynamic certificate pinning](https://approov.io/docs/latest/approov-usage-documentation/#dynamic-pinning) setup, out of the box. - -> **NOTE:** By default the pin is extracted from the public key of the leaf certificate served by the domain, as visible to the box issuing the Approov CLI command and the Approov servers. - -### Approov Secret - -Approov tokens are signed with a symmetric secret. To verify tokens, we need to grab the secret using the [Approov secret command](https://approov.io/docs/latest/approov-cli-tool-reference/#secret-command) and plug it into the NodeJS API server environment to check the signatures of the [Approov Tokens](https://www.approov.io/docs/latest/approov-usage-documentation/#approov-tokens) that it processes. - -Retrieve the Approov secret with: - -```bash -approov secret -get base64 -``` - -> **NOTE:** The `approov secret` command requires an [administration role](https://approov.io/docs/latest/approov-usage-documentation/#account-access-roles) to execute successfully. - -#### Set the Approov Secret - -Open the `.env` file and add the Approov secret to the var: - -```bash -APPROOV_BASE64_SECRET=approov_base64_secret_here -``` - -[TOC](#toc---table-of-contents) - - -## Approov Token Check - -To check the Approov token we will use the [auth0/node-jsonwebtoken](https://github.com/auth0/node-jsonwebtoken#readme) package, but you are free to use another one of your preference. - -Add this function to your code: - -```javascript -const verifyApproovToken = function(req) { - - const approovToken = req.headers['approov-token'] - - if (!approovToken) { - // You may want to add some logging here. - return false - } - - // decode token, verify secret and check exp - return jwt.verify(approovToken, approovSecret, { algorithms: ['HS256'] }, function(err, decoded) { - - if (err) { - // You may want to add some logging here. - return false - } - - // The Approov token was successfully verified. We will add the claims to - // the request object to allow further use of them during the request - // processing. - req.approovTokenClaims = decoded - - return true - }) -} -``` - -Now you just need to invoke it from the endpoint you want to protected: - -```javascript -if (!verifyApproovToken(req)) { - res.statusCode = 401 - res.end(JSON.stringify({})) - return -} -``` - -> **NOTE:** When the Approov token validation fails we return a `401` with an empty body, because we don't want to give clues to an attacker about the reason the request failed, and you can go even further by returning a `400`. - -A full working example for a simple Hello World server can be found at [src/approov-protected-server/token-check](/src/approov-protected-server/token-check). - -[TOC](#toc---table-of-contents) - - -## Test your Approov Integration - -The following examples below use cURL, but you can also use the [Postman Collection](/README.md#testing-with-postman) to make the API requests. Just remember that you need to adjust the urls and tokens defined in the collection to match your deployment. Alternatively, the above README also contains instructions for using the preset _dummy_ secret to test your Approov integration. - -#### With Valid Approov Tokens - -Generate a valid token example from the Approov Cloud service: - -```bash -approov token -genExample your.api.domain.com -``` - -Then make the request with the generated token: - -```bash -curl -i --request GET 'https://your.api.domain.com' \ - --header 'Approov-Token: APPROOV_TOKEN_EXAMPLE_HERE' -``` - -The request should be accepted. For example: - -```text -HTTP/2 200 - -... - -{"message": "Hello, World!"} -``` - -#### With Invalid Approov Tokens - -Generate an invalid token example from the Approov Cloud service: - -```bash -approov token -type invalid -genExample your.api.domain.com -``` - -Then make the request with the generated token: - -```bash -curl -i --request GET 'https://your.api.domain.com' \ - --header 'Approov-Token: APPROOV_INVALID_TOKEN_EXAMPLE_HERE' -``` - -The above request should fail with an Unauthorized error. For example: - -```text -HTTP/2 401 - -... - -{} -``` - -[TOC](#toc---table-of-contents) diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..c51d547 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,15 @@ +{ + "name": "approov-node-token-check", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "approov-node-token-check", + "version": "1.0.0", + "engines": { + "node": ">=18.0.0" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..3ceab8f --- /dev/null +++ b/package.json @@ -0,0 +1,14 @@ +{ + "name": "approov-node-token-check", + "version": "1.0.0", + "private": true, + "description": "Approov token and token binding verification example using Node.js core HTTP.", + "main": "ApproovApplication.js", + "scripts": { + "start": "node ApproovApplication.js", + "test": "bash test.sh" + }, + "engines": { + "node": ">=18.0.0" + } +} diff --git a/run_server.sh b/run_server.sh new file mode 100755 index 0000000..5a64feb --- /dev/null +++ b/run_server.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash +# Host-side wrapper: sets FOLLOW_LOGS (default true) and hands execution to scripts/build.sh, +# which handles image build/run plus container log tailing. +set -euo pipefail + +FOLLOW_LOGS="${FOLLOW_LOGS:-true}" ./scripts/build.sh diff --git a/scripts/build.sh b/scripts/build.sh new file mode 100755 index 0000000..cf13e41 --- /dev/null +++ b/scripts/build.sh @@ -0,0 +1,87 @@ +#!/usr/bin/env bash +# Dual-purpose orchestrator: on the host it builds/runs the Docker container, +# and inside the container it starts the application command with optional +# readiness checks and log attachment. +set -euo pipefail + +requirement_check() { # verify command exists on PATH + local cmd="$1" + if ! command -v "$cmd" >/dev/null 2>&1; then + fail "Missing required command: ${cmd}" + fi +} +fail() { echo "ERROR: $*" >&2; exit 1; } # uniform error output + exit +info() { echo "info $*"; } # lightweight logging helper + +# Globals configured via environment overrides +RUN_MODE="${RUN_MODE:-host}" # host orchestrator vs container entrypoint +APP_START_CMD="${APP_START_CMD:-}" # command executed when inside container +FOLLOW_LOGS="${FOLLOW_LOGS:-true}" # toggle docker logs -f attachment +HOST_PORT="${HOST_PORT:-8080}" # host-facing port (e.g., http://localhost:3000) +WAIT_URL="${WAIT_URL:-http://localhost:${HOST_PORT}/approov-state}" # readiness probe target +WAIT_TIMEOUT="${WAIT_TIMEOUT:-60}" # how long to wait before failing readiness +WAIT_INTERVAL="${WAIT_INTERVAL:-2}" # delay between readiness checks +CONTAINER_PORT="${CONTAINER_PORT:-$HOST_PORT}" # container listener, defaults to host port +IMAGE_NAME="${IMAGE_NAME:-approov-quickstart-nodejs}" +CONTAINER_NAME="${CONTAINER_NAME:-approov-quickstart-nodejs-app}" +ENV_FILE="${ENV_FILE:-.env}" +RUNTIME_BIN_DIR="${RUNTIME_BIN_DIR:-}" # optional runtime-specific bin path + +in_container() { + [[ "$RUN_MODE" == "container" ]] || [[ -f "/.dockerenv" ]] +} + +if in_container; then + [[ -n "$APP_START_CMD" ]] || fail "APP_START_CMD must be provided to run the server" + if [[ -n "$RUNTIME_BIN_DIR" ]]; then + export PATH="${RUNTIME_BIN_DIR}:$PATH" # e.g., RUNTIME_BIN_DIR=/usr/local/go/bin to expose runtime binaries for golang + fi + info "Container starting application: ${APP_START_CMD}" + exec bash -c "$APP_START_CMD" +fi + +requirement_check docker +if ! command -v approov >/dev/null 2>&1; then + info "Approov CLI not found; continuing without CLI checks (tests may need it)" +fi + +[[ -f "$ENV_FILE" ]] || fail "$ENV_FILE not found. Run cp .env.example .env first." +[[ -f Dockerfile ]] || fail "Dockerfile not found in $(pwd)" + +if docker ps -a --format '{{.Names}}' | grep -Fxq "$CONTAINER_NAME"; then + info "Removing stale container ${CONTAINER_NAME}" + docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true +fi + +info "Building ${IMAGE_NAME}" +docker build -t "$IMAGE_NAME" . || fail "Docker build failed" + +info "Starting ${CONTAINER_NAME} on host port ${HOST_PORT}, container port ${CONTAINER_PORT}" +docker run -d \ + --name "$CONTAINER_NAME" \ + --env-file "$ENV_FILE" \ + -e RUN_MODE=container \ + -p "${HOST_PORT}:${CONTAINER_PORT}" \ + "$IMAGE_NAME" >/dev/null || fail "Failed to start container ${CONTAINER_NAME}" + +wait_for_service() { + local url="$1" timeout="$2" interval="$3" elapsed=0 + info "Waiting for application to become ready at ${url}" + until curl -fsS "$url" >/dev/null 2>&1; do + sleep "$interval" + elapsed=$((elapsed + interval)) + if (( elapsed >= timeout )); then + fail "Application did not become ready within ${timeout}s (last url: ${url})" + fi + done + info "Application is ready" +} + +wait_for_service "$WAIT_URL" "$WAIT_TIMEOUT" "$WAIT_INTERVAL" + +if [[ "$FOLLOW_LOGS" == "true" ]]; then + info "Container logs (Ctrl+C to stop):" + docker logs -f "$CONTAINER_NAME" +else + info "Skipping container logs attachment." +fi diff --git a/scripts/install-prerequisites.sh b/scripts/install-prerequisites.sh new file mode 100755 index 0000000..96b4fd4 --- /dev/null +++ b/scripts/install-prerequisites.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash +# Installs base OS dependencies (and optionally language runtimes) required for +# the quickstart image; intended for use inside the Docker build context. +set -euo pipefail + +requirement_check() { # helper to verify command availability + local cmd="$1" + if ! command -v "$cmd" >/dev/null 2>&1; then + fail "Missing required command: ${cmd}" + fi +} +fail() { echo "ERROR: $*" >&2; exit 1; } # helper to abort with message +info() { echo "info $*"; } # helper to print informational logs + +# ensure apt operations have privileges +[[ "$(id -u)" -eq 0 ]] || fail "Run this script as root (or via sudo) inside the build context." + +# base utilities required by every quickstart +apt-get update +apt-get install -y --no-install-recommends \ + ca-certificates \ + curl \ + build-essential \ + git + +# Optional runtime install; set INSTALL_LANGUAGE_RUNTIME=true per quickstart and fill commands below. +INSTALL_LANGUAGE_RUNTIME_FLAG="${INSTALL_LANGUAGE_RUNTIME:-false}" + +if [[ "$INSTALL_LANGUAGE_RUNTIME_FLAG" == "true" ]]; then + info "Installing language/runtime specific dependencies" + # TODO: add language-specific install commands (apt packages, curl tarballs, etc.) +fi diff --git a/src/approov-protected-server/token-binding-check/.env.example b/src/approov-protected-server/token-binding-check/.env.example deleted file mode 100644 index bd6a3fc..0000000 --- a/src/approov-protected-server/token-binding-check/.env.example +++ /dev/null @@ -1,12 +0,0 @@ -# The port for the Quickstart Postman collection and cURL examples is 8002 -HTTP_PORT=8002 - -# Setting to 0.0.0.0 is only necessary to run from inside a docker container -SERVER_HOSTNAME=0.0.0.0 - -# For production usage the secret is always retrieved with the Approov CLI tool, that can be also used to generate valid -# tokens for testing purposes. Check docs at https://approov.io/docs/v2.1/approov-cli-tool-reference/#token-commands. -# -# But if you don't have the Approov CLI tool, you can still test the backend with Postman or similar, by creating a -# secret with `openssl rand -base64 64 | tr -d '\n'; echo`, and afterwards you can use jwt.io to create the JWT token. -APPROOV_BASE64_SECRET=approov_base64_secret_here diff --git a/src/approov-protected-server/token-binding-check/README.md b/src/approov-protected-server/token-binding-check/README.md deleted file mode 100644 index 112c9cb..0000000 --- a/src/approov-protected-server/token-binding-check/README.md +++ /dev/null @@ -1,102 +0,0 @@ -# Approov Token Binding Integration Example - -This Approov integration example is from where the code example for the [Approov token binding check quickstart](/docs/APPROOV_TOKEN_BINDING_QUICKSTART.md) is extracted, and you can use it as a playground to better understand how simple and easy it is to implement [Approov](https://approov.io) in a NodeJS API server. - - -## TOC - Table of Contents - -* [Why?](#why) -* [How it Works?](#how-it-works) -* [Requirements](#requirements) -* [Try the Approov Integration Example](#try-the-approov-integration-example) - - -## Why? - -To lock down your API server to your mobile app. Please read the brief summary in the [Approov Overview](/OVERVIEW.md#why) at the root of this repo or visit our [website](https://approov.io/product/) for more details. - -[TOC](#toc---table-of-contents) - - -## How it works? - -The NodeJS server is very simple and is defined in the file [src/approov-protected-server/token-binding-check/hello-server-protected.js](src/approov-protected-server/token-binding-check/hello-server-protected.js). Take a look at the `verifyApproovToken()` and `verifyApproovTokenBinding()` functions to see the simple code for the checks. - -For more background on Approov, see the [Approov Overview](/OVERVIEW.md#how-it-works) at the root of this repo. - -[TOC](#toc---table-of-contents) - - -## Requirements - -To run this example you will need to have NodeJS installed. If you don't have then please follow the official installation instructions from [here](https://nodejs.org/en/download/) to download and install it. - -[TOC](#toc---table-of-contents) - - -## Try the Approov Integration Example - -First, you need to set the dummy secret in the `src/approov-protected-server/token-binding-check/.env` file as explained [here](/TESTING.md#the-dummy-secret). - -Second, you need to install the dependencies. From the `src/approov-protected-server/token-binding-check` folder execute: - -```bash -npm install -``` - -Now, you can run this example from the `src/approov-protected-server/token-binding-check` folder with: - -```bash -npm start -``` - -Next, you can test that it works with: - -```bash -curl -iX GET 'http://localhost:8002' -``` - -The response will be a `401` unauthorized request: - -```text -HTTP/1.1 401 Unauthorized -Content-Type: application/json -Date: Wed, 06 Apr 2022 12:01:58 GMT -Connection: keep-alive -Keep-Alive: timeout=5 -Content-Length: 2 - -{} -``` - -The reason you got a `401` is because the Approoov token isn't provided in the headers of the request. - -Finally, you can test that the Approov integration example works as expected with this [Postman collection](/TESTING.md#testing-with-postman) or with some cURL requests [examples](/TESTING.md#testing-with-curl). - -[TOC](#toc---table-of-contents) - - -## Issues - -If you find any issue while following our instructions then just report it [here](https://github.com/approov/quickstart-nodejs-token-check/issues), with the steps to reproduce it, and we will sort it out and/or guide you to the correct path. - - -[TOC](#toc---table-of-contents) - - -## Useful Links - -If you wish to explore the Approov solution in more depth, then why not try one of the following links as a jumping off point: - -* [Approov Free Trial](https://approov.io/signup)(no credit card needed) -* [Approov Get Started](https://approov.io/product/demo) -* [Approov QuickStarts](https://approov.io/docs/latest/approov-integration-examples/) -* [Approov Docs](https://approov.io/docs) -* [Approov Blog](https://approov.io/blog/) -* [Approov Resources](https://approov.io/resource/) -* [Approov Customer Stories](https://approov.io/customer) -* [Approov Support](https://approov.io/contact) -* [About Us](https://approov.io/company) -* [Contact Us](https://approov.io/contact) - -[TOC](#toc---table-of-contents) diff --git a/src/approov-protected-server/token-binding-check/hello-server-protected.js b/src/approov-protected-server/token-binding-check/hello-server-protected.js deleted file mode 100644 index e065a6a..0000000 --- a/src/approov-protected-server/token-binding-check/hello-server-protected.js +++ /dev/null @@ -1,101 +0,0 @@ -const http = require('http'); -const dotenv = require('dotenv').config() -const jwt = require('jsonwebtoken') -const crypto = require('crypto') - -if (dotenv.error) { - console.debug('FAILED TO PARSE `.env` FILE | ' + dotenv.error) -} - -// To run in a docker container add to the .env file `SERVER_HOSTNAME=0.0.0.0`. -const hostname = dotenv.parsed.SERVER_HOSTNAME || 'localhost'; - -// The port for the Quickstart Postman collection and cURL examples is 8002 -const port = dotenv.parsed.HTTP_PORT || 8002; - -const approovBase64Secret = dotenv.parsed.APPROOV_BASE64_SECRET; - -const approovSecret = Buffer.from(approovBase64Secret, 'base64') - -const verifyApproovToken = function(req) { - - const approovToken = req.headers['approov-token'] - - if (!approovToken) { - // You may want to add some logging here. - console.debug("Missing Approov token") - return false - } - - // decode token, verify secret and check exp - return jwt.verify(approovToken, approovSecret, { algorithms: ['HS256'] }, function(err, decoded) { - - if (err) { - // You may want to add some logging here. - console.debug("Approov token error: " + err) - return false - } - - // The Approov token was successfully verified. We will add the claims to - // the request object to allow further use of them during the request - // processing. - req.approovTokenClaims = decoded - - return true - }) -} - -const verifyApproovTokenBinding = function(req) { - - if (!("pay" in req.approovTokenClaims)) { - // You may want to add some logging here. - console.debug("Approov token binding error: the `pay` claim missing in the payload") - return false - } - - // The Approov token claims is added to the request object on a successful - // Approov token verification. See `verifyApproovToken()` function. - token_binding_claim = req.approovTokenClaims.pay - - // 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. - token_binding_header = req.headers['authorization'] - - if (!token_binding_header) { - // You may want to add some logging here. - console.debug("Approov token binding error: missing the token binding header in the request") - return false - } - - // We need to hash and base64 encode the token binding header, because thats - // 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') - - return token_binding_claim === token_binding_header_encoded -} - -const server = http.createServer((req, res) => { - console.debug("<--- / GET") - - res.setHeader('Content-Type', 'application/json'); - - if (!verifyApproovToken(req)) { - res.statusCode = 401 - res.end(JSON.stringify({})) - return - } - - if (!verifyApproovTokenBinding(req)) { - console.debug("Approov token binding error: invalid token binding") - res.statusCode = 401 - res.end(JSON.stringify({})) - return - } - - res.statusCode = 200; - res.end(JSON.stringify({message: "Hello, World!"})) -}); - -server.listen(port, hostname, () => { - console.log(`Approov protected server running at http://${hostname}:${port}/`); -}); diff --git a/src/approov-protected-server/token-binding-check/package-lock.json b/src/approov-protected-server/token-binding-check/package-lock.json deleted file mode 100644 index e81c7bc..0000000 --- a/src/approov-protected-server/token-binding-check/package-lock.json +++ /dev/null @@ -1,218 +0,0 @@ -{ - "name": "approov-nodejs-quickstart", - "version": "1.0.0", - "lockfileVersion": 2, - "requires": true, - "packages": { - "": { - "name": "approov-nodejs-quickstart", - "version": "1.0.0", - "license": "MIT", - "dependencies": { - "dotenv": "^8.2.0", - "jsonwebtoken": "^9.0.0" - }, - "devDependencies": {} - }, - "node_modules/buffer-equal-constant-time": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", - "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" - }, - "node_modules/dotenv": { - "version": "8.6.0", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.6.0.tgz", - "integrity": "sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g==", - "engines": { - "node": ">=10" - } - }, - "node_modules/ecdsa-sig-formatter": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", - "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", - "dependencies": { - "safe-buffer": "^5.0.1" - } - }, - "node_modules/jsonwebtoken": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.0.tgz", - "integrity": "sha512-tuGfYXxkQGDPnLJ7SibiQgVgeDgfbPq2k2ICcbgqW8WxWLBAxKQM/ZCu/IT8SOSwmaYl4dpTFCW5xZv7YbbWUw==", - "dependencies": { - "jws": "^3.2.2", - "lodash": "^4.17.21", - "ms": "^2.1.1", - "semver": "^7.3.8" - }, - "engines": { - "node": ">=12", - "npm": ">=6" - } - }, - "node_modules/jwa": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", - "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", - "dependencies": { - "buffer-equal-constant-time": "1.0.1", - "ecdsa-sig-formatter": "1.0.11", - "safe-buffer": "^5.0.1" - } - }, - "node_modules/jws": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", - "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", - "dependencies": { - "jwa": "^1.4.1", - "safe-buffer": "^5.0.1" - } - }, - "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" - }, - "node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" - } - }, - "dependencies": { - "buffer-equal-constant-time": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", - "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" - }, - "dotenv": { - "version": "8.6.0", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.6.0.tgz", - "integrity": "sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g==" - }, - "ecdsa-sig-formatter": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", - "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", - "requires": { - "safe-buffer": "^5.0.1" - } - }, - "jsonwebtoken": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.0.tgz", - "integrity": "sha512-tuGfYXxkQGDPnLJ7SibiQgVgeDgfbPq2k2ICcbgqW8WxWLBAxKQM/ZCu/IT8SOSwmaYl4dpTFCW5xZv7YbbWUw==", - "requires": { - "jws": "^3.2.2", - "lodash": "^4.17.21", - "ms": "^2.1.1", - "semver": "^7.3.8" - } - }, - "jwa": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", - "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", - "requires": { - "buffer-equal-constant-time": "1.0.1", - "ecdsa-sig-formatter": "1.0.11", - "safe-buffer": "^5.0.1" - } - }, - "jws": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", - "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", - "requires": { - "jwa": "^1.4.1", - "safe-buffer": "^5.0.1" - } - }, - "lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" - }, - "lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "requires": { - "yallist": "^4.0.0" - } - }, - "ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" - }, - "safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" - }, - "semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "requires": { - "lru-cache": "^6.0.0" - } - }, - "yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" - } - } -} diff --git a/src/approov-protected-server/token-binding-check/package.json b/src/approov-protected-server/token-binding-check/package.json deleted file mode 100644 index cc46170..0000000 --- a/src/approov-protected-server/token-binding-check/package.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "name": "approov-nodejs-quickstart", - "version": "1.0.0", - "description": "Approov quickstart for NodeJS without using a framework.", - "main": "src/index.js", - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1", - "start": "node hello-server-protected.js" - }, - "author": "paulos@criticalblue.com", - "license": "MIT", - "dependencies": { - "dotenv": "^8.2.0", - "jsonwebtoken": "^9.0.0" - }, - "devDependencies": {} -} diff --git a/src/approov-protected-server/token-check/.env.example b/src/approov-protected-server/token-check/.env.example deleted file mode 100644 index bd6a3fc..0000000 --- a/src/approov-protected-server/token-check/.env.example +++ /dev/null @@ -1,12 +0,0 @@ -# The port for the Quickstart Postman collection and cURL examples is 8002 -HTTP_PORT=8002 - -# Setting to 0.0.0.0 is only necessary to run from inside a docker container -SERVER_HOSTNAME=0.0.0.0 - -# For production usage the secret is always retrieved with the Approov CLI tool, that can be also used to generate valid -# tokens for testing purposes. Check docs at https://approov.io/docs/v2.1/approov-cli-tool-reference/#token-commands. -# -# But if you don't have the Approov CLI tool, you can still test the backend with Postman or similar, by creating a -# secret with `openssl rand -base64 64 | tr -d '\n'; echo`, and afterwards you can use jwt.io to create the JWT token. -APPROOV_BASE64_SECRET=approov_base64_secret_here diff --git a/src/approov-protected-server/token-check/README.md b/src/approov-protected-server/token-check/README.md deleted file mode 100644 index 3ed4595..0000000 --- a/src/approov-protected-server/token-check/README.md +++ /dev/null @@ -1,101 +0,0 @@ -# Approov Token Integration Example - -This Approov integration example is from where the code example for the [Approov token check quickstart](/docs/APPROOV_TOKEN_QUICKSTART.md) is extracted, and you can use it as a playground to better understand how simple and easy it is to implement [Approov](https://approov.io) in a NodeJS API server. - -## TOC - Table of Contents - -* [Why?](#why) -* [How it Works?](#how-it-works) -* [Requirements](#requirements) -* [Try the Approov Integration Example](#try-the-approov-integration-example) - - -## Why? - -To lock down your API server to your mobile app. Please read the brief summary in the [Approov Overview](/OVERVIEW.md#why) at the root of this repo or visit our [website](https://approov.io/product/) for more details. - -[TOC](#toc---table-of-contents) - - -## How it works? - -The NodeJS server is very simple and is defined in the file [src/approov-protected-server/token-check/hello-server-protected.js](src/approov-protected-server/token-check/hello-server-protected.js). Take a look at the `verifyApproovToken()` function to see the simple code for the check. - -For more background on Approov, see the [Approov Overview](/OVERVIEW.md#how-it-works) at the root of this repo. - -[TOC](#toc---table-of-contents) - - -## Requirements - -To run this example you will need to have NodeJS installed. If you don't have then please follow the official installation instructions from [here](https://nodejs.org/en/download/) to download and install it. - -[TOC](#toc---table-of-contents) - - -## Try the Approov Integration Example - -First, you need to set the dummy secret in the `src/approov-protected-server/token-check/.env` file as explained [here](/TESTING.md#the-dummy-secret). - -Second, you need to install the dependencies. From the `src/approov-protected-server/token-check` folder execute: - -```bash -npm install -``` - -Now, you can run this example from the `src/approov-protected-server/token-check` folder with: - -```bash -npm start -``` - -Next, you can test that it works with: - -```bash -curl -iX GET 'http://localhost:8002' -``` - -The response will be a `401` unauthorized request: - -```text -HTTP/1.1 401 Unauthorized -Content-Type: application/json -Date: Wed, 06 Apr 2022 12:01:58 GMT -Connection: keep-alive -Keep-Alive: timeout=5 -Content-Length: 2 - -{} -``` - -The reason you got a `401` is because the Approoov token isn't provided in the headers of the request. - -Finally, you can test that the Approov integration example works as expected with this [Postman collection](/TESTING.md#testing-with-postman) or with some cURL requests [examples](/TESTING.md#testing-with-curl). - -[TOC](#toc---table-of-contents) - - -## Issues - -If you find any issue while following our instructions then just report it [here](https://github.com/approov/quickstart-nodejs-token-check/issues), with the steps to reproduce it, and we will sort it out and/or guide you to the correct path. - - -[TOC](#toc---table-of-contents) - - -## Useful Links - -If you wish to explore the Approov solution in more depth, then why not try one of the following links as a jumping off point: - -* [Approov Free Trial](https://approov.io/signup)(no credit card needed) -* [Approov Get Started](https://approov.io/product/demo) -* [Approov QuickStarts](https://approov.io/docs/latest/approov-integration-examples/) -* [Approov Docs](https://approov.io/docs) -* [Approov Blog](https://approov.io/blog/) -* [Approov Resources](https://approov.io/resource/) -* [Approov Customer Stories](https://approov.io/customer) -* [Approov Support](https://approov.io/contact) -* [About Us](https://approov.io/company) -* [Contact Us](https://approov.io/contact) - -[TOC](#toc---table-of-contents) diff --git a/src/approov-protected-server/token-check/hello-server-protected.js b/src/approov-protected-server/token-check/hello-server-protected.js deleted file mode 100644 index e887afa..0000000 --- a/src/approov-protected-server/token-check/hello-server-protected.js +++ /dev/null @@ -1,66 +0,0 @@ -const http = require('http'); -const dotenv = require('dotenv').config() -const jwt = require('jsonwebtoken') - - -if (dotenv.error) { - console.debug('FAILED TO PARSE `.env` FILE | ' + dotenv.error) -} - -// To run in a docker container add to the .env file `SERVER_HOSTNAME=0.0.0.0`. -const hostname = dotenv.parsed.SERVER_HOSTNAME || 'localhost'; - -// The port for the Quickstart Postman collection and cURL examples is 8002 -const port = dotenv.parsed.HTTP_PORT || 8002; - -const approovBase64Secret = dotenv.parsed.APPROOV_BASE64_SECRET; - -const approovSecret = Buffer.from(approovBase64Secret, 'base64') - -const verifyApproovToken = function(req) { - - const approovToken = req.headers['approov-token'] - - if (!approovToken) { - // You may want to add some logging here. - console.debug("Missing Approov token") - return false - } - - // decode token, verify secret and check exp - return jwt.verify(approovToken, approovSecret, { algorithms: ['HS256'] }, function(err, decoded) { - - if (err) { - // You may want to add some logging here. - console.debug("Approov token error: " + err) - return false - } - - // The Approov token was successfully verified. We will add the claims to - // the request object to allow further use of them during the request - // processing. - req.approovTokenClaims = decoded - - return true - }) -} - -const server = http.createServer((req, res) => { - - console.debug("<--- / GET") - - res.setHeader('Content-Type', 'application/json'); - - if (!verifyApproovToken(req)) { - res.statusCode = 401 - res.end(JSON.stringify({})) - return - } - - res.statusCode = 200; - res.end(JSON.stringify({message: "Hello, World!"})) -}); - -server.listen(port, hostname, () => { - console.log(`Approov protected server running at http://${hostname}:${port}/`); -}); diff --git a/src/approov-protected-server/token-check/package-lock.json b/src/approov-protected-server/token-check/package-lock.json deleted file mode 100644 index e81c7bc..0000000 --- a/src/approov-protected-server/token-check/package-lock.json +++ /dev/null @@ -1,218 +0,0 @@ -{ - "name": "approov-nodejs-quickstart", - "version": "1.0.0", - "lockfileVersion": 2, - "requires": true, - "packages": { - "": { - "name": "approov-nodejs-quickstart", - "version": "1.0.0", - "license": "MIT", - "dependencies": { - "dotenv": "^8.2.0", - "jsonwebtoken": "^9.0.0" - }, - "devDependencies": {} - }, - "node_modules/buffer-equal-constant-time": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", - "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" - }, - "node_modules/dotenv": { - "version": "8.6.0", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.6.0.tgz", - "integrity": "sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g==", - "engines": { - "node": ">=10" - } - }, - "node_modules/ecdsa-sig-formatter": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", - "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", - "dependencies": { - "safe-buffer": "^5.0.1" - } - }, - "node_modules/jsonwebtoken": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.0.tgz", - "integrity": "sha512-tuGfYXxkQGDPnLJ7SibiQgVgeDgfbPq2k2ICcbgqW8WxWLBAxKQM/ZCu/IT8SOSwmaYl4dpTFCW5xZv7YbbWUw==", - "dependencies": { - "jws": "^3.2.2", - "lodash": "^4.17.21", - "ms": "^2.1.1", - "semver": "^7.3.8" - }, - "engines": { - "node": ">=12", - "npm": ">=6" - } - }, - "node_modules/jwa": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", - "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", - "dependencies": { - "buffer-equal-constant-time": "1.0.1", - "ecdsa-sig-formatter": "1.0.11", - "safe-buffer": "^5.0.1" - } - }, - "node_modules/jws": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", - "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", - "dependencies": { - "jwa": "^1.4.1", - "safe-buffer": "^5.0.1" - } - }, - "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" - }, - "node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" - } - }, - "dependencies": { - "buffer-equal-constant-time": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", - "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" - }, - "dotenv": { - "version": "8.6.0", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.6.0.tgz", - "integrity": "sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g==" - }, - "ecdsa-sig-formatter": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", - "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", - "requires": { - "safe-buffer": "^5.0.1" - } - }, - "jsonwebtoken": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.0.tgz", - "integrity": "sha512-tuGfYXxkQGDPnLJ7SibiQgVgeDgfbPq2k2ICcbgqW8WxWLBAxKQM/ZCu/IT8SOSwmaYl4dpTFCW5xZv7YbbWUw==", - "requires": { - "jws": "^3.2.2", - "lodash": "^4.17.21", - "ms": "^2.1.1", - "semver": "^7.3.8" - } - }, - "jwa": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.1.tgz", - "integrity": "sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==", - "requires": { - "buffer-equal-constant-time": "1.0.1", - "ecdsa-sig-formatter": "1.0.11", - "safe-buffer": "^5.0.1" - } - }, - "jws": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", - "integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==", - "requires": { - "jwa": "^1.4.1", - "safe-buffer": "^5.0.1" - } - }, - "lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" - }, - "lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "requires": { - "yallist": "^4.0.0" - } - }, - "ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" - }, - "safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" - }, - "semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "requires": { - "lru-cache": "^6.0.0" - } - }, - "yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" - } - } -} diff --git a/src/approov-protected-server/token-check/package.json b/src/approov-protected-server/token-check/package.json deleted file mode 100644 index cc46170..0000000 --- a/src/approov-protected-server/token-check/package.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "name": "approov-nodejs-quickstart", - "version": "1.0.0", - "description": "Approov quickstart for NodeJS without using a framework.", - "main": "src/index.js", - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1", - "start": "node hello-server-protected.js" - }, - "author": "paulos@criticalblue.com", - "license": "MIT", - "dependencies": { - "dotenv": "^8.2.0", - "jsonwebtoken": "^9.0.0" - }, - "devDependencies": {} -} diff --git a/src/unprotected-server/.env.example b/src/unprotected-server/.env.example deleted file mode 100644 index 85c98af..0000000 --- a/src/unprotected-server/.env.example +++ /dev/null @@ -1,5 +0,0 @@ -# The port for the Quickstart Postman collection and cURL examples is 8002 -HTTP_PORT=8002 - -# Setting to 0.0.0.0 is only necessary to run from inside a docker container -SERVER_HOSTNAME=0.0.0.0 diff --git a/src/unprotected-server/README.md b/src/unprotected-server/README.md deleted file mode 100644 index 9af372d..0000000 --- a/src/unprotected-server/README.md +++ /dev/null @@ -1,98 +0,0 @@ -# Unprotected Server Example - -The unprotected example is the base reference to build the [Approov protected servers](/src/approov-protected-server/). This a very basic Hello World server. - - -## TOC - Table of Contents - -* [Why?](#why) -* [How it Works?](#how-it-works) -* [Requirements](#requirements) -* [Try It](#try-it) - - -## Why? - -To be the starting building block for the [Approov protected servers](/src/approov-protected-server/), that will show you how to lock down your API server to your mobile app. Please read the brief summary in the [Approov Overview](/OVERVIEW.md#why) at the root of this repo or visit our [website](https://approov.io/product/) for more details. - -[TOC](#toc---table-of-contents) - - -## How it works? - -The NodeJS server is very simple and is defined in the file [src/unprotected-server/hello-server-unprotected.js](/src/unprotected-server/hello-server-unprotected.js). - -The server only replies to the endpoint `/` with the message: - -```json -{"message": "Hello, World!"} -``` - -[TOC](#toc---table-of-contents) - - -## Requirements - -To run this example you will need to have NodeJS installed. If you don't have then please follow the official installation instructions from [here](https://nodejs.org/en/download/) to download and install it. - -[TOC](#toc---table-of-contents) - - -## Try It - -First install the dependencies. From the `src/unprotected-server` folder execute: - -```text -npm install -``` - -Now, you can run this example from the `src/unprotected-server` folder with: - -```text -npm start -``` - -Finally, you can test that it works with: - -```text -curl -iX GET 'http://localhost:8002' -``` - -The response will be: - -```text -HTTP/1.1 200 OK -Content-Type: application/json -Date: Tue, 08 Sep 2020 16:05:53 GMT -Content-Length: 28 - -{"message": "Hello, World!"} -``` - -[TOC](#toc---table-of-contents) - - -## Issues - -If you find any issue while following our instructions then just report it [here](https://github.com/approov/quickstart-nodejs-token-check/issues), with the steps to reproduce it, and we will sort it out and/or guide you to the correct path. - - -[TOC](#toc---table-of-contents) - - -## Useful Links - -If you wish to explore the Approov solution in more depth, then why not try one of the following links as a jumping off point: - -* [Approov Free Trial](https://approov.io/signup)(no credit card needed) -* [Approov Get Started](https://approov.io/product/demo) -* [Approov QuickStarts](https://approov.io/docs/latest/approov-integration-examples/) -* [Approov Docs](https://approov.io/docs) -* [Approov Blog](https://approov.io/blog/) -* [Approov Resources](https://approov.io/resource/) -* [Approov Customer Stories](https://approov.io/customer) -* [Approov Support](https://approov.io/contact) -* [About Us](https://approov.io/company) -* [Contact Us](https://approov.io/contact) - -[TOC](#toc---table-of-contents) diff --git a/src/unprotected-server/hello-server-unprotected.js b/src/unprotected-server/hello-server-unprotected.js deleted file mode 100644 index 1980eb1..0000000 --- a/src/unprotected-server/hello-server-unprotected.js +++ /dev/null @@ -1,22 +0,0 @@ -const http = require('http'); -const dotenv = require('dotenv').config() - -if (dotenv.error) { - console.debug('FAILED TO PARSE `.env` FILE | ' + dotenv.error) -} - -// To run in a docker container add to the .env file `SERVER_HOSTNAME=0.0.0.0`. -const hostname = dotenv.parsed.SERVER_HOSTNAME || 'localhost'; - -// The port for the Quickstart Postman collection and cURL examples is 8002 -const port = dotenv.parsed.HTTP_PORT || 8002; - -const server = http.createServer((req, res) => { - res.statusCode = 200; - res.setHeader('Content-Type', 'application/json'); - res.end(JSON.stringify({message: "Hello, World!"})) -}); - -server.listen(port, hostname, () => { - console.log(`Server running at http://${hostname}:${port}/`); -}); diff --git a/src/unprotected-server/package-lock.json b/src/unprotected-server/package-lock.json deleted file mode 100644 index ed1116b..0000000 --- a/src/unprotected-server/package-lock.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "name": "approov-nodejs-quickstart", - "version": "1.0.0", - "lockfileVersion": 2, - "requires": true, - "packages": { - "": { - "name": "approov-nodejs-quickstart", - "version": "1.0.0", - "license": "MIT", - "dependencies": { - "dotenv": "^8.2.0" - }, - "devDependencies": {} - }, - "node_modules/dotenv": { - "version": "8.6.0", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.6.0.tgz", - "integrity": "sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g==", - "engines": { - "node": ">=10" - } - } - }, - "dependencies": { - "dotenv": { - "version": "8.6.0", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-8.6.0.tgz", - "integrity": "sha512-IrPdXQsk2BbzvCBGBOTmmSH5SodmqZNt4ERAZDmW4CT+tL8VtvinqywuANaFu4bOMWki16nqf0e4oC0QIaDr/g==" - } - } -} diff --git a/src/unprotected-server/package.json b/src/unprotected-server/package.json deleted file mode 100644 index 2c63656..0000000 --- a/src/unprotected-server/package.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "name": "approov-nodejs-quickstart", - "version": "1.0.0", - "description": "Approov quickstart for NodeJS without using a framework.", - "main": "src/index.js", - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1", - "start": "node hello-server-unprotected.js" - }, - "author": "paulos@criticalblue.com", - "license": "MIT", - "dependencies": { - "dotenv": "^8.2.0" - }, - "devDependencies": {} -} diff --git a/test.sh b/test.sh new file mode 100644 index 0000000..eb04b4b --- /dev/null +++ b/test.sh @@ -0,0 +1,382 @@ +#!/bin/bash +set -o errexit +set -o nounset +set -o pipefail + +####################################### +# Approov demo API test harness. +# +# Description: +# Calls unprotected and protected endpoints of the Approov demo API, +# validates HTTP status codes and logs complete HTTP exchanges +# (request + response) to a timestamped log file. +# +# Dependencies: +# - bash +# - curl +# - approov CLI available on PATH and configured +# +# Environment: +# BASE_URL: +# Base URL of the API under test. Default: http://localhost:8080 +# TOKDIR: +# Directory where temporary token files are stored. Default: .config +# LOGDIR=${TOKDIR}/logs, LOGFILE=${LOGDIR}/.log +####################################### + +# Constants +readonly BASE_URL="${BASE_URL:-http://localhost:8080}" +readonly TOKDIR="${TOKDIR:-.config}" +readonly LOGDIR="${TOKDIR}/logs" +readonly LOGFILE="${LOGDIR}/$(date '+%Y-%m-%d_%H-%M-%S').log" + +# Globals +# is_approov_disabled: +# Boolean flag indicating if Approov checks appear disabled +# based on /approov-state endpoint. +is_approov_disabled=false +success_code=200 +failure_code=401 + +# state_http_code: +# HTTP status code from /approov-state endpoint. +state_http_code='' + +####################################### +# Print error message to STDERR with timestamp. +# Globals: +# None +# Arguments: +# All arguments are printed as the error message. +# Outputs: +# Writes formatted error message to STDERR. +# Returns: +# 0 +####################################### +err() { + echo "[$(date +'%Y-%m-%dT%H:%M:%S%z')]: $*" >&2 +} + +####################################### +# Ensure a required command exists on PATH. +# Globals: +# None +# Arguments: +# command name to check. +# Outputs: +# Error message to STDERR if command is missing. +# Returns: +# Exits the script with code 1 if the command is missing. +####################################### +requirement_check() { + local cmd="$1" + + if ! command -v "${cmd}" >/dev/null 2>&1; then + err "Missing required command: ${cmd}" + exit 1 + fi +} + +####################################### +# Generate an Approov token into an output file. +# Globals: +# None +# Arguments: +# output file path. +# arguments passed to "approov token". +# Outputs: +# Captures stdout+stderr from "approov token", takes the last non-empty +# line as the token, and writes only that line to the output file. +# Returns: +# 0 on success. +# 1 on failure (CLI error or no token produced). +####################################### +gen_token() { + local outfile="$1" + shift + + set +o errexit + local cli_output + cli_output="$(approov token "$@" 2>&1)" + local rc=$? + set -o errexit + + if ((rc != 0)); then + err "Approov CLI failed: approov token $*" + printf '%s\n' "${cli_output}" >&2 + return 1 + fi + + # Prints notices before the token, grab the last non-empty line. + local token + token="$(printf '%s\n' "${cli_output}" | awk 'NF{last=$0} END{print last}')" + if [[ -z "${token}" ]]; then + err "Approov CLI produced no token output" + return 1 + fi + + printf '%s\n' "${token}" >"${outfile}" +} + +####################################### +# Print test result and append full HTTP exchange to a log file. +# Globals: +# LOGFILE +# is_approov_disabled +# Arguments: +# $1 - test name. +# $2 - expected HTTP status code. +# $3 - actual HTTP status code. +# $4 - full HTTP response (headers + body). +# Outputs: +# Human-readable result to STDOUT. +# Detailed log entry appended to LOGFILE. +# Returns: +# 0 +####################################### +print_test_result() { + local name="$1" + local expected="$2" + local status="$3" + local resp="$4" + + local result="Failed" + if [[ "${status}" == "${expected}" ]]; then + result="Passed" + fi + + echo "${name}: ${result} (status: ${status}, expected: ${expected})" + + { + echo "Test: ${name}" + echo "Expected status: ${expected}" + echo "Actual status: ${status}" + if [[ "${is_approov_disabled}" == "false" ]]; then + echo "Approov State: enabled, token checks performed." + else + echo "Approov State: disabled, no checks performed." + fi + echo + echo "HTTP exchange:" + echo "${resp}" + echo + } >>"${LOGFILE}" 2>&1 +} + +####################################### +# Execute a curl call for a test and evaluate the result. +# Globals: +# None +# Notes: +# Uses print_test_result, which logs to LOGFILE and reads +# is_approov_disabled. +# Arguments: +# test name. +# expected HTTP status code. +# arguments passed to curl. +# Outputs: +# Short result to STDOUT, full HTTP exchange appended to LOGFILE. +# Returns: +# 0 on success, curl's exit code on failure. +####################################### +run_test() { + # shift after each grab so $1 advances (name -> expected -> rest) + local name="$1"; shift + local expected="$1"; shift + + local resp + local status + local curl_rc + + # -i: include headers, -s: silent + set +o errexit + resp="$(curl -i -s "$@")" + curl_rc=$? + set -o errexit + + if ((curl_rc != 0)); then + err "curl failed for ${name} (rc=${curl_rc})" + return "${curl_rc}" + fi + + status="$( + printf '%s\n' "${resp}" | + grep -m1 '^HTTP/' | + awk '{print $2}' + )" + + print_test_result "${name}" "${expected}" "${status}" "${resp}" +} + +main() { + requirement_check "approov" + requirement_check "curl" + + mkdir -p "${TOKDIR}" "${LOGDIR}" + + echo "Listing Approov API configuration:" + approov api -list + echo + + echo "Approov state check:" + local state_response + state_response="$(curl -i -s "${BASE_URL}/approov-state")" + state_http_code="$( + printf '%s\n' "${state_response}" | + grep -m1 '^HTTP/' | + awk '{print $2}' + )" + + if [[ "${state_http_code}" != "200" || -z "${state_http_code}" ]]; then + err "Failed to get Approov state from ${BASE_URL}/approov-state (status=${state_http_code:-unknown})" + exit 1 + fi + + if grep -q '"approovEnabled":true' <<<"${state_response}"; then + echo " Approov service: ENABLED" + is_approov_disabled=false + else + echo " Approov service: DISABLED" + is_approov_disabled=true + failure_code=200 + fi + echo + + # 0) Unprotected endpoint. + run_test \ + "Unprotected request - no approov protection" \ + "${success_code}" \ + "${BASE_URL}/unprotected" + + # 1) Token check. + gen_token \ + "${TOKDIR}/approov_token_valid" \ + -genExample \ + example.com + + # 1.1) Valid Token. + run_test \ + "Token check - valid token" \ + "${success_code}" \ + -H "approov-token: $(<"${TOKDIR}/approov_token_valid")" \ + "${BASE_URL}/token-check" + + # 1.2) Invalid Token. + gen_token \ + "${TOKDIR}/approov_token_invalid" \ + -genExample \ + example.com \ + -type invalid || true + + run_test \ + "Token check - invalid token" \ + "${failure_code}" \ + -H "approov-token: $(<"${TOKDIR}/approov_token_invalid")" \ + "${BASE_URL}/token-check" + + # 2) Token Binding ["Authorization"]. + local AUTH_VAL="ExampleAuthToken==" + export HASH_INPUT="${AUTH_VAL}" + + gen_token \ + "${TOKDIR}/approov_token_bind_auth_valid" \ + -setDataHashInToken "${HASH_INPUT}" \ + -genExample \ + example.com + + # 2.1) Valid Token. + run_test \ + "Single Binding - valid token and header" \ + "${success_code}" \ + -H "Authorization: ${AUTH_VAL}" \ + -H "approov-token: $(<"${TOKDIR}/approov_token_bind_auth_valid")" \ + "${BASE_URL}/token-binding" + + # 2.2) Missing Header. + run_test \ + "Single Binding - missing Authorization header" \ + "${failure_code}" \ + -H "approov-token: $(<"${TOKDIR}/approov_token_bind_auth_valid")" \ + "${BASE_URL}/token-binding" + + # 2.3) Incorrect Header. + run_test \ + "Single Binding - incorrect Authorization header" \ + "${failure_code}" \ + -H "Authorization: BadAuthToken==" \ + -H "approov-token: $(<"${TOKDIR}/approov_token_bind_auth_valid")" \ + "${BASE_URL}/token-binding" + + # 2.4) Invalid Token. + gen_token \ + "${TOKDIR}/approov_token_bind_auth_invalid" \ + -setDataHashInToken "${HASH_INPUT}" \ + -genExample \ + example.com \ + -type invalid || true + + run_test \ + "Single Binding - invalid token" \ + "${failure_code}" \ + -H "Authorization: ${AUTH_VAL}" \ + -H "approov-token: $(<"${TOKDIR}/approov_token_bind_auth_invalid")" \ + "${BASE_URL}/token-binding" + + # 3) Token Binding ["Authorization", "Content-Digest"]. + local AUTH_VAL2="ExampleAuthToken==" + local CD_VAL="ContentDigest==" + export HASH_INPUT="${AUTH_VAL2}${CD_VAL}" + + gen_token \ + "${TOKDIR}/approov_token_bind_auth_cd_valid" \ + -setDataHashInToken "${HASH_INPUT}" \ + -genExample \ + example.com + + # 3.1) Valid. + run_test \ + "Double Binding - valid token and headers" \ + "${success_code}" \ + -H "Authorization: ${AUTH_VAL2}" \ + -H "Content-Digest: ${CD_VAL}" \ + -H "approov-token: $(<"${TOKDIR}/approov_token_bind_auth_cd_valid")" \ + "${BASE_URL}/token-double-binding" + + # 3.2) Missing headers. + run_test \ + "Double Binding - missing binding headers" \ + "${failure_code}" \ + -H "approov-token: $(<"${TOKDIR}/approov_token_bind_auth_cd_valid")" \ + "${BASE_URL}/token-double-binding" + + # 3.3) Incorrect headers. + run_test \ + "Double Binding - incorrect binding headers" \ + "${failure_code}" \ + -H "Authorization: BadAuthToken==" \ + -H "Content-Digest: BadContentDigest==" \ + -H "approov-token: $(<"${TOKDIR}/approov_token_bind_auth_cd_valid")" \ + "${BASE_URL}/token-double-binding" + + # 3.4) Invalid token. + gen_token \ + "${TOKDIR}/approov_token_bind_auth_cd_invalid" \ + -setDataHashInToken "${HASH_INPUT}" \ + -genExample \ + example.com \ + -type invalid || true + + run_test \ + "Double Binding - invalid token" \ + "${failure_code}" \ + -H "Authorization: ${AUTH_VAL2}" \ + -H "Content-Digest: ${CD_VAL}" \ + -H "approov-token: $(<"${TOKDIR}/approov_token_bind_auth_cd_invalid")" \ + "${BASE_URL}/token-double-binding" + + echo + echo "Full request and response details are saved in:" + echo " ${LOGFILE}" +} + +main "$@" From 5f64a834165084eae45c721b61e5b1c070b6401f Mon Sep 17 00:00:00 2001 From: kmilejMAC Date: Fri, 16 Jan 2026 16:46:26 +0000 Subject: [PATCH 2/8] Initial working Node.js --- README.md | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/README.md b/README.md index 67d8295..1b0784e 100644 --- a/README.md +++ b/README.md @@ -7,12 +7,7 @@ This project provides a server-side example of Approov token verification for a - `/token-binding` - requires a valid Approov token which is bound to a header value. - `/token-double-binding` - requires a valid Approov token which is bound to two header values. -In this example, Approov protection is enforced by `approovTokenVerifier` (see [ApproovApplication.js](ApproovApplication.js#L199-L235)), which reads the `Approov-Token`, verifies its signature/exp, and validates token bindings before returning `401` on failure. Protected endpoints are the routes listed in `ROUTES` (see [ApproovApplication.js](ApproovApplication.js#L32-L88)), which are only accessible when this middleware passes. - -Implementation pointers (with line ranges): -- Middleware/filter implementation: [ApproovApplication.js](ApproovApplication.js#L199-L235) -- Protected route configuration: [ApproovApplication.js](ApproovApplication.js#L32-L88) -- Token/binding validation logic (`verifyApproovToken`, `extractBindingValue`, `isBindingValid`): [ApproovApplication.js](ApproovApplication.js#L237-L303). +In this example, Approov protection is enforced by [approovTokenVerifier](https://github.com/approov/quickstart-nodejs-token-check/blob/refactor/nodejs-quickstart/ApproovApplication.js#L199-L235), which reads the `Approov-Token` header, and validates the token `signature` and `exp` claim in [verifyApproovToken](https://github.com/approov/quickstart-nodejs-token-check/blob/refactor/nodejs-quickstart/ApproovApplication.js#L237-L251). Protected endpoints are those with `requiresApproov: true` in [ROUTES](https://github.com/approov/quickstart-nodejs-token-check/blob/refactor/nodejs-quickstart/ApproovApplication.js#L32-L88). ## Approov Token Verification Flow From eb45013beeafc34e6b96b9f3d3ab7fb8a5d58969 Mon Sep 17 00:00:00 2001 From: kmilejMAC Date: Wed, 4 Feb 2026 12:14:31 +0000 Subject: [PATCH 3/8] add structured logging and update token binding logic --- ApproovApplication.js | 240 ++++++++++++++++++++++++++----- README.md | 30 +++- scripts/install-prerequisites.sh | 32 ----- test.sh | 24 ++-- 4 files changed, 236 insertions(+), 90 deletions(-) delete mode 100755 scripts/install-prerequisites.sh diff --git a/ApproovApplication.js b/ApproovApplication.js index bd92aa7..dc2e51e 100644 --- a/ApproovApplication.js +++ b/ApproovApplication.js @@ -12,21 +12,15 @@ const HTTP_PORT = parsePort(process.env.HTTP_PORT, 8080); const APPROOV_SECRET_BASE64URL = process.env.APPROOV_BASE64URL_SECRET || process.env.APPROOV_BASE64_SECRET; - -if (!hasText(APPROOV_SECRET_BASE64URL)) { - console.error('APPROOV_BASE64URL_SECRET environment variable is not set'); - process.exit(1); -} - -const APPROOV_SECRET = base64UrlDecodeToBuffer(APPROOV_SECRET_BASE64URL.trim()); +const APPROOV_SECRET = resolveApproovSecret(APPROOV_SECRET_BASE64URL); let approovEnabled = true; let tokenBindingEnabled = true; const HEADER_NAMES = Object.freeze({ - APPROOV_TOKEN: 'approov-token', - AUTHORIZATION: 'authorization', - CONTENT_DIGEST: 'content-digest', + APPROOV_TOKEN: 'Approov-Token', + AUTHORIZATION: 'Authorization', + SESSION_ID: 'SessionId', }); const ROUTES = Object.freeze([ @@ -78,12 +72,14 @@ const ROUTES = Object.freeze([ path: '/token-binding', handler: tokenBindingHandler, requiresApproov: true, + bindingHeaders: [HEADER_NAMES.AUTHORIZATION], }, { method: 'GET', path: '/token-double-binding', handler: tokenDoubleBindingHandler, requiresApproov: true, + bindingHeaders: [HEADER_NAMES.AUTHORIZATION, HEADER_NAMES.SESSION_ID,], }, ]); @@ -111,6 +107,7 @@ const server = http.createServer((req, res) => { headers: req.headers, state: {}, }; + initializeRequestState(ctx); try { runMiddleware(ctx, MIDDLEWARE, route.handler); @@ -141,22 +138,26 @@ function approovStateHandler(ctx) { function enableApproovHandler(ctx) { approovEnabled = true; tokenBindingEnabled = true; + syncRequestState(ctx); writeJson(ctx.res, 200, statePayload()); } function disableApproovHandler(ctx) { approovEnabled = false; tokenBindingEnabled = false; + syncRequestState(ctx); writeJson(ctx.res, 200, statePayload()); } function enableTokenBindingHandler(ctx) { tokenBindingEnabled = true; + syncRequestState(ctx); writeJson(ctx.res, 200, statePayload()); } function disableTokenBindingHandler(ctx) { tokenBindingEnabled = false; + syncRequestState(ctx); writeJson(ctx.res, 200, statePayload()); } @@ -187,12 +188,12 @@ function tokenBindingHandler(ctx) { function tokenDoubleBindingHandler(ctx) { const authorization = headerValue(ctx.headers, HEADER_NAMES.AUTHORIZATION); - const contentDigest = headerValue(ctx.headers, HEADER_NAMES.CONTENT_DIGEST); + const sessionId = headerValue(ctx.headers, HEADER_NAMES.SESSION_ID); const response = infoPayload( "Protected endpoint '/token-double-binding'; dual token binding enforced." ); response.authorizationHeaderPresent = hasText(authorization); - response.contentDigestHeaderPresent = hasText(contentDigest); + response.sessionIdHeaderPresent = hasText(sessionId); writeJson(ctx.res, 200, response); } @@ -210,7 +211,7 @@ function approovTokenVerifier(ctx, next) { const token = readApproovToken(ctx.headers); if (!hasText(token)) { - unauthorized(ctx.res, 'Missing Approov-Token header.'); + unauthorized(ctx, 'missing_approov_token', 'Missing Approov-Token header.'); return; } @@ -218,14 +219,19 @@ function approovTokenVerifier(ctx, next) { try { claims = verifyApproovToken(token); } catch (error) { - unauthorized(ctx.res, error.message); + unauthorized(ctx, 'token_verification_failed', error.message); return; } - if (tokenBindingEnabled && needsBindingCheck(route.path)) { - const bindingValue = extractBindingValue(route.path, ctx.headers); - if (!hasText(bindingValue) || !isBindingValid(bindingValue, claims)) { - unauthorized(ctx.res, 'Invalid token binding.'); + const bindingHeaders = normalizeBindingHeaders(route.bindingHeaders); + if (tokenBindingEnabled && bindingHeaders.length > 0) { + const bindingValue = extractBindingValue(ctx.headers, bindingHeaders); + if (!hasText(bindingValue)) { + unauthorized(ctx, 'missing_binding_header', 'Missing binding header.'); + return; + } + if (!isBindingValid(bindingValue, claims)) { + unauthorized(ctx, 'binding_mismatch', 'Invalid token binding.'); return; } } @@ -269,27 +275,25 @@ function parseJwt(token) { }; } -function needsBindingCheck(pathname) { - return pathname === '/token-binding' || pathname === '/token-double-binding'; -} - -function extractBindingValue(pathname, headers) { - if (pathname === '/token-binding') { - return trimOrNull(headerValue(headers, HEADER_NAMES.AUTHORIZATION)); +function normalizeBindingHeaders(bindingHeaders) { + if (!Array.isArray(bindingHeaders)) { + return []; } + return bindingHeaders + .map((header) => (typeof header === 'string' ? header.trim() : '')) + .filter((header) => hasText(header)); +} - const authorization = trimOrNull( - headerValue(headers, HEADER_NAMES.AUTHORIZATION) - ); - const digest = trimOrNull( - headerValue(headers, HEADER_NAMES.CONTENT_DIGEST) - ); - - if (!hasText(authorization) || !hasText(digest)) { - return null; +function extractBindingValue(headers, bindingHeaders) { + const values = []; + for (const header of bindingHeaders) { + const value = trimOrNull(headerValue(headers, header)); + if (!hasText(value)) { + return null; + } + values.push(value); } - - return authorization + digest; + return values.join(''); } function isBindingValid(bindingValue, claims) { @@ -314,6 +318,66 @@ function validateExpiration(claims) { } } +function initializeRequestState(ctx) { + syncRequestState(ctx); + ctx.state.logSummary = defaultSummaryForRoute(ctx.route, ctx.state); + ctx.res.on('finish', () => { + const status = ctx.res.statusCode; + if (status === 200 || status === 401) { + logRequestCompleted(ctx); + } + }); +} + +function syncRequestState(ctx) { + ctx.state.approovEnabled = approovEnabled; + ctx.state.tokenBindingEnabled = tokenBindingEnabled; +} + +function defaultSummaryForRoute(route, state) { + if (!route || !route.requiresApproov) { + return 'ok'; + } + if (!state.approovEnabled) { + return 'approov_disabled'; + } + return 'approov_ok'; +} + +function logRequestCompleted(ctx) { + const state = ctx.state; + const summary = ctx.state.logSummary || defaultSummaryForRoute(ctx.route, state); + const socket = ctx.req.socket; + const fields = { + summary, + method: ctx.method, + path: ctx.path, + status: ctx.res.statusCode, + ip: normalizeIp(socket?.remoteAddress), + port: socket?.localPort ?? HTTP_PORT, + approovEnabled: state.approovEnabled, + tokenBindingEnabled: state.tokenBindingEnabled, + required_headers: requiredHeadersForRoute(ctx.route, state), + }; + logInfo('http.request.completed', fields); +} + +function requiredHeadersForRoute(route, state) { + if (!route || !route.requiresApproov || !state.approovEnabled) { + return []; + } + const required = [HEADER_NAMES.APPROOV_TOKEN]; + if (state.tokenBindingEnabled) { + const bindingHeaders = normalizeBindingHeaders(route.bindingHeaders); + for (const header of bindingHeaders) { + if (!required.includes(header)) { + required.push(header); + } + } + } + return required; +} + function statePayload() { return { approovEnabled, @@ -333,7 +397,10 @@ function readApproovToken(headers) { } function headerValue(headers, name) { - const value = headers[name]; + if (!headers || typeof name !== 'string') { + return undefined; + } + const value = headers[name.toLowerCase()]; return Array.isArray(value) ? value[0] : value; } @@ -361,8 +428,9 @@ function parseRequest(req) { }; } -function unauthorized(res, message) { - writeJson(res, 401, { error: 'unauthorized', message }); +function unauthorized(ctx, reason, message) { + ctx.state.logSummary = `approov_failed:${reason}`; + writeJson(ctx.res, 401, { error: 'unauthorized', message }); } function writeJson(res, statusCode, payload) { @@ -375,6 +443,58 @@ function writeJson(res, statusCode, payload) { res.end(body); } +function logInfo(event, fields) { + const line = formatLogLine(event, fields); + console.log(line); +} + +function logError(event, fields) { + const line = formatLogLine(event, fields); + console.error(line); +} + +function formatLogLine(event, fields) { + const timestamp = formatTimestamp(new Date()); + const formattedFields = formatLogFields(fields); + return `[${timestamp}] ${event} ${formattedFields}`; +} + +function formatLogFields(fields) { + return Object.entries(fields) + .map(([key, value]) => `"${key}":${formatLogValue(value)}`) + .join(','); +} + +function formatLogValue(value) { + if (value === undefined) { + return 'null'; + } + return JSON.stringify(value); +} + +function formatTimestamp(date) { + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, '0'); + const day = String(date.getDate()).padStart(2, '0'); + const hours = String(date.getHours()).padStart(2, '0'); + const minutes = String(date.getMinutes()).padStart(2, '0'); + const seconds = String(date.getSeconds()).padStart(2, '0'); + return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`; +} + +function normalizeIp(value) { + if (!hasText(value)) { + return 'unknown'; + } + if (value === '::1') { + return '127.0.0.1'; + } + if (value.startsWith('::ffff:')) { + return value.slice(7); + } + return value; +} + function signHmac(value, secret) { return crypto.createHmac('sha256', secret).update(value).digest(); } @@ -424,6 +544,48 @@ function base64UrlDecodeToBuffer(value) { return Buffer.from(padded, 'base64'); } +function resolveApproovSecret(value) { + const placeholder = 'approov_base64url_secret_here'; + if (!hasText(value) || value.trim() === placeholder) { + logError('approov.config', { summary: 'Required secret is not set' }); + process.exit(1); + } + + try { + return decodeBase64Secret(value.trim()); + } catch (error) { + logError('approov.config', { + summary: 'Required secret is invalid', + error: error instanceof Error ? error.message : String(error), + }); + process.exit(1); + } +} + +function decodeBase64Secret(value) { + if (!/^[A-Za-z0-9+/_-]+={0,2}$/.test(value)) { + throw new Error('Secret contains invalid characters.'); + } + + const normalized = value.replace(/-/g, '+').replace(/_/g, '/'); + const padded = normalized.padEnd( + normalized.length + ((4 - (normalized.length % 4)) % 4), + '=' + ); + const buffer = Buffer.from(padded, 'base64'); + if (buffer.length === 0) { + throw new Error('Secret is empty after decoding.'); + } + + const reencoded = buffer.toString('base64').replace(/=+$/g, ''); + const normalizedNoPad = normalized.replace(/=+$/g, ''); + if (reencoded !== normalizedNoPad) { + throw new Error('Secret is not valid base64.'); + } + + return buffer; +} + function parsePort(value, fallback) { if (!hasText(value)) { return fallback; diff --git a/README.md b/README.md index 1b0784e..03bedcb 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,23 @@ This project provides a server-side example of Approov token verification for a - `/token-binding` - requires a valid Approov token which is bound to a header value. - `/token-double-binding` - requires a valid Approov token which is bound to two header values. -In this example, Approov protection is enforced by [approovTokenVerifier](https://github.com/approov/quickstart-nodejs-token-check/blob/refactor/nodejs-quickstart/ApproovApplication.js#L199-L235), which reads the `Approov-Token` header, and validates the token `signature` and `exp` claim in [verifyApproovToken](https://github.com/approov/quickstart-nodejs-token-check/blob/refactor/nodejs-quickstart/ApproovApplication.js#L237-L251). Protected endpoints are those with `requiresApproov: true` in [ROUTES](https://github.com/approov/quickstart-nodejs-token-check/blob/refactor/nodejs-quickstart/ApproovApplication.js#L32-L88). +In this example, Approov token check is implemented in `ApproovApplication.js`. The responsibilities break down as follows: + +1. **JWT Approov Token validation (signature + expiry)** is implemented in [`verifyApproovToken`](https://github.com/approov/quickstart-nodejs-token-check/blob/refactor/nodejs-quickstart/ApproovApplication.js#L243-L257) and [`validateExpiration`](https://github.com/approov/quickstart-nodejs-token-check/blob/refactor/nodejs-quickstart/ApproovApplication.js#L309-L319). +It verifies the HS256 signature and rejects tokens that are missing or past `exp`. + +2. **Token binding (pay + hash)** is handled by [`isBindingValid`](https://github.com/approov/quickstart-nodejs-token-check/blob/refactor/nodejs-quickstart/ApproovApplication.js#L299-L307) and [`hashBase64`](https://github.com/approov/quickstart-nodejs-token-check/blob/refactor/nodejs-quickstart/ApproovApplication.js#L502-L507). +It computes `base64(sha256(binding_value))` and compares it to `pay` with a timing-safe check. + +3. **Middleware enforcement** is done by [`approovTokenVerifier`](https://github.com/approov/quickstart-nodejs-token-check/blob/refactor/nodejs-quickstart/ApproovApplication.js#L200-L241). +Requests without valid token or binding are rejected with `401`. + +4. **Binding value selection (what gets hashed)** is in [`normalizeBindingHeaders`](https://github.com/approov/quickstart-nodejs-token-check/blob/refactor/nodejs-quickstart/ApproovApplication.js#L278-L285) and [`extractBindingValue`](ApproovApplication.js#L287-L297). +It uses the headers configured on each protected route (currently `Authorization` for single binding, or `Authorization` + `SessionId` for double binding). + +5. **Protected route requirements** are defined in the [`ROUTES` table](https://github.com/approov/quickstart-nodejs-token-check/blob/refactor/nodejs-quickstart/ApproovApplication.js#L26-L84) via the `requiresApproov` and `bindingHeaders` fields. + +6. **Protected routes are registered** in the [`ROUTE_TABLE` map](https://github.com/approov/quickstart-nodejs-token-check/blob/refactor/nodejs-quickstart/ApproovApplication.js#L86-L88) and resolved in the HTTP server handler [`createServer`](https://github.com/approov/quickstart-nodejs-token-check/blob/refactor/nodejs-quickstart/ApproovApplication.js#L92-L114). ## Approov Token Verification Flow @@ -78,7 +94,7 @@ bash test.sh This script: - Verifies that the `approov` and `curl` commands are installed. - Checks Approov status by calling `/approov-state` (enabled vs disabled). -- Runs endpoint tests against `/unprotected` (no token), `/token-check` (valid/invalid Approov tokens), `/token-binding` (token bound to `Authorization`), and `/token-double-binding` (token bound to `Authorization` + `Content-Digest`). +- Runs endpoint tests against `/unprotected` (no token), `/token-check` (valid/invalid Approov tokens), `/token-binding` (token bound to `Authorization`), and `/token-double-binding` (token bound to `Authorization` + `SessionId`). - Logs full request/response details to `.config/logs/.log`. #### *1. Unprotected Endpoint (No Approov)* @@ -175,16 +191,16 @@ Cache-Control: no-cache - The client sends three headers on authenticated API calls: - `Approov-Token` - `Authorization` - - `Content-Digest` It is combined with the `Authorization` header to create a stronger binding. + - `SessionId` It is combined with the `Authorization` header to create a stronger binding. - Both are included in the hash inside the Approov token. This means the server verifies a single hash that covers both authentication credentials. - **Use case:** Stronger protection then single binding by tying both headers together. ***The following example shows how the API responds when an Approov token with two bindings is required.*** -*Generate a valid Approov token bound to the `Authorization` and `Content-Digest` headers:* +*Generate a valid Approov token bound to the `Authorization` and `SessionId` headers:* ```bash -approov token -setDataHashInToken ExampleAuthToken==ContentDigest== -genExample example.com +approov token -setDataHashInToken ExampleAuthToken==123 -genExample example.com ``` *Use the generated token with two bindings in the Approov-Token and Authorization headers when calling the `/token-double-binding` endpoint.* @@ -193,7 +209,7 @@ approov token -setDataHashInToken ExampleAuthToken==ContentDigest== -genExample curl -iX GET http://localhost:8080/token-double-binding \ -H "Approov-Token: valid_approov_token_here" \ -H "Authorization: ExampleAuthToken==" \ - -H "Content-Digest: ContentDigest==" + -H "SessionId: 123" ``` The response will be `200 OK` for this request. @@ -239,4 +255,4 @@ If you encounter any problems while following this guide, or have any other conc * [Approov Customer Stories](https://approov.io/customer) * [Approov Support](https://approov.io/info/technical-support) * [About Us](https://approov.io/company) -* [Contact Us](https://approov.io/info/contact) \ No newline at end of file +* [Contact Us](https://approov.io/info/contact) diff --git a/scripts/install-prerequisites.sh b/scripts/install-prerequisites.sh deleted file mode 100755 index 96b4fd4..0000000 --- a/scripts/install-prerequisites.sh +++ /dev/null @@ -1,32 +0,0 @@ -#!/usr/bin/env bash -# Installs base OS dependencies (and optionally language runtimes) required for -# the quickstart image; intended for use inside the Docker build context. -set -euo pipefail - -requirement_check() { # helper to verify command availability - local cmd="$1" - if ! command -v "$cmd" >/dev/null 2>&1; then - fail "Missing required command: ${cmd}" - fi -} -fail() { echo "ERROR: $*" >&2; exit 1; } # helper to abort with message -info() { echo "info $*"; } # helper to print informational logs - -# ensure apt operations have privileges -[[ "$(id -u)" -eq 0 ]] || fail "Run this script as root (or via sudo) inside the build context." - -# base utilities required by every quickstart -apt-get update -apt-get install -y --no-install-recommends \ - ca-certificates \ - curl \ - build-essential \ - git - -# Optional runtime install; set INSTALL_LANGUAGE_RUNTIME=true per quickstart and fill commands below. -INSTALL_LANGUAGE_RUNTIME_FLAG="${INSTALL_LANGUAGE_RUNTIME:-false}" - -if [[ "$INSTALL_LANGUAGE_RUNTIME_FLAG" == "true" ]]; then - info "Installing language/runtime specific dependencies" - # TODO: add language-specific install commands (apt packages, curl tarballs, etc.) -fi diff --git a/test.sh b/test.sh index eb04b4b..a5b8b28 100644 --- a/test.sh +++ b/test.sh @@ -322,13 +322,13 @@ main() { -H "approov-token: $(<"${TOKDIR}/approov_token_bind_auth_invalid")" \ "${BASE_URL}/token-binding" - # 3) Token Binding ["Authorization", "Content-Digest"]. + # 3) Token Binding ["Authorization", "SessionId"]. local AUTH_VAL2="ExampleAuthToken==" - local CD_VAL="ContentDigest==" - export HASH_INPUT="${AUTH_VAL2}${CD_VAL}" + local SI_VAL="123" + export HASH_INPUT="${AUTH_VAL2}${SI_VAL}" gen_token \ - "${TOKDIR}/approov_token_bind_auth_cd_valid" \ + "${TOKDIR}/approov_token_bind_auth_si_valid" \ -setDataHashInToken "${HASH_INPUT}" \ -genExample \ example.com @@ -338,15 +338,15 @@ main() { "Double Binding - valid token and headers" \ "${success_code}" \ -H "Authorization: ${AUTH_VAL2}" \ - -H "Content-Digest: ${CD_VAL}" \ - -H "approov-token: $(<"${TOKDIR}/approov_token_bind_auth_cd_valid")" \ + -H "SessionId: ${SI_VAL}" \ + -H "approov-token: $(<"${TOKDIR}/approov_token_bind_auth_si_valid")" \ "${BASE_URL}/token-double-binding" # 3.2) Missing headers. run_test \ "Double Binding - missing binding headers" \ "${failure_code}" \ - -H "approov-token: $(<"${TOKDIR}/approov_token_bind_auth_cd_valid")" \ + -H "approov-token: $(<"${TOKDIR}/approov_token_bind_auth_si_valid")" \ "${BASE_URL}/token-double-binding" # 3.3) Incorrect headers. @@ -354,13 +354,13 @@ main() { "Double Binding - incorrect binding headers" \ "${failure_code}" \ -H "Authorization: BadAuthToken==" \ - -H "Content-Digest: BadContentDigest==" \ - -H "approov-token: $(<"${TOKDIR}/approov_token_bind_auth_cd_valid")" \ + -H "SessionId: Bad123" \ + -H "approov-token: $(<"${TOKDIR}/approov_token_bind_auth_si_valid")" \ "${BASE_URL}/token-double-binding" # 3.4) Invalid token. gen_token \ - "${TOKDIR}/approov_token_bind_auth_cd_invalid" \ + "${TOKDIR}/approov_token_bind_auth_si_invalid" \ -setDataHashInToken "${HASH_INPUT}" \ -genExample \ example.com \ @@ -370,8 +370,8 @@ main() { "Double Binding - invalid token" \ "${failure_code}" \ -H "Authorization: ${AUTH_VAL2}" \ - -H "Content-Digest: ${CD_VAL}" \ - -H "approov-token: $(<"${TOKDIR}/approov_token_bind_auth_cd_invalid")" \ + -H "SessionId: ${SI_VAL}" \ + -H "approov-token: $(<"${TOKDIR}/approov_token_bind_auth_si_invalid")" \ "${BASE_URL}/token-double-binding" echo From 3c2999d4c82b0cde16a3d2699e5d13fe1f1a4471 Mon Sep 17 00:00:00 2001 From: kmilejMAC Date: Wed, 4 Feb 2026 12:23:55 +0000 Subject: [PATCH 4/8] add structured logging and update token binding logic --- run_server.sh | 6 ------ 1 file changed, 6 deletions(-) delete mode 100755 run_server.sh diff --git a/run_server.sh b/run_server.sh deleted file mode 100755 index 5a64feb..0000000 --- a/run_server.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env bash -# Host-side wrapper: sets FOLLOW_LOGS (default true) and hands execution to scripts/build.sh, -# which handles image build/run plus container log tailing. -set -euo pipefail - -FOLLOW_LOGS="${FOLLOW_LOGS:-true}" ./scripts/build.sh From 19505d1807ea24768ba04c8a4b80d34cc6b3beb0 Mon Sep 17 00:00:00 2001 From: kmilejMAC Date: Wed, 4 Feb 2026 12:29:09 +0000 Subject: [PATCH 5/8] add run-server.sh --- run-server.sh | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100755 run-server.sh diff --git a/run-server.sh b/run-server.sh new file mode 100755 index 0000000..5a64feb --- /dev/null +++ b/run-server.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash +# Host-side wrapper: sets FOLLOW_LOGS (default true) and hands execution to scripts/build.sh, +# which handles image build/run plus container log tailing. +set -euo pipefail + +FOLLOW_LOGS="${FOLLOW_LOGS:-true}" ./scripts/build.sh From 79607dbaa04f475bf2325ae15fcf200356f996f9 Mon Sep 17 00:00:00 2001 From: kmilejMAC Date: Fri, 20 Feb 2026 15:27:40 +0000 Subject: [PATCH 6/8] Refactor auth errors and token-binding validation - ApproovApplication.js: added centralized error handling (HttpError + handleRequestError) and return 400 for invalid request URL. - approovTokenVerifier now passes auth failures to middleware flow (next(error)) for missing Approov-Token, invalid token, and binding mismatch. - Token binding now uses constructTokenBindingInput(req, bindingHeaders) and generateTokenBindingHash(), with explicit 401 missing_binding_header when a required header is absent. - scripts/build.sh now validates APPROOV_BASE64URL_SECRET in .env (rejects empty/placeholder values) before starting Docker. - scripts/build.sh now prints container logs on startup failure; runtime moved to Node 24 (package engines + Docker base image), and .dockerignore + LICENSE were added. --- .dockerignore | 16 ++++ ApproovApplication.js | 210 ++++++++++++++++++++++++++++++++++-------- Dockerfile | 6 +- LICENSE | 21 +++++ README.md | 16 ++-- package-lock.json | 2 +- package.json | 2 +- scripts/build.sh | 55 ++++++++++- 8 files changed, 275 insertions(+), 53 deletions(-) create mode 100644 .dockerignore create mode 100644 LICENSE diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..209d9a8 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,16 @@ +# Dependencies and package manager logs +node_modules +npm-debug.log* +yarn-error.log* + +# Environment files (keep example for docs/reference) +.env +.env.* +!.env.example + +# Local metadata and tooling artifacts +.git +.gitignore +.DS_Store +.config +*.log diff --git a/ApproovApplication.js b/ApproovApplication.js index dc2e51e..eaf371f 100644 --- a/ApproovApplication.js +++ b/ApproovApplication.js @@ -90,31 +90,45 @@ const ROUTE_TABLE = new Map( const MIDDLEWARE = Object.freeze([approovTokenVerifier]); const server = http.createServer((req, res) => { - const requestInfo = parseRequest(req); - const route = ROUTE_TABLE.get(`${requestInfo.method} ${requestInfo.path}`); - - if (!route) { - writeJson(res, 404, { error: 'not_found' }); - return; - } - const ctx = { req, res, - route, - method: requestInfo.method, - path: requestInfo.path, + route: null, + method: (req.method || 'GET').toUpperCase(), + path: '/', headers: req.headers, state: {}, }; initializeRequestState(ctx); + let requestInfo; try { - runMiddleware(ctx, MIDDLEWARE, route.handler); + requestInfo = parseRequest(req); } catch (error) { - console.error('Unhandled error:', error); - writeJson(res, 500, { error: 'server_error' }); + handleRequestError(ctx, error); + return; } + + ctx.method = requestInfo.method; + ctx.path = requestInfo.path; + ctx.route = ROUTE_TABLE.get(`${ctx.method} ${ctx.path}`); + ctx.state.logSummary = defaultSummaryForRoute(ctx.route, ctx.state); + + if (!ctx.route) { + handleRequestError( + ctx, + createHttpError(404, 'not_found', 'Requested endpoint was not found.', { + logSummary: 'not_found', + }) + ); + return; + } + + runMiddleware(ctx, MIDDLEWARE, ctx.route.handler, (error) => { + if (error) { + handleRequestError(ctx, error); + } + }); }); server.listen(HTTP_PORT, SERVER_HOSTNAME, () => { @@ -211,7 +225,12 @@ function approovTokenVerifier(ctx, next) { const token = readApproovToken(ctx.headers); if (!hasText(token)) { - unauthorized(ctx, 'missing_approov_token', 'Missing Approov-Token header.'); + next( + createUnauthorizedError( + 'missing_approov_token', + 'Missing Approov-Token header.' + ) + ); return; } @@ -219,19 +238,23 @@ function approovTokenVerifier(ctx, next) { try { claims = verifyApproovToken(token); } catch (error) { - unauthorized(ctx, 'token_verification_failed', error.message); + next( + createUnauthorizedError('token_verification_failed', error.message) + ); return; } const bindingHeaders = normalizeBindingHeaders(route.bindingHeaders); if (tokenBindingEnabled && bindingHeaders.length > 0) { - const bindingValue = extractBindingValue(ctx.headers, bindingHeaders); - if (!hasText(bindingValue)) { - unauthorized(ctx, 'missing_binding_header', 'Missing binding header.'); + let bindingInput; + try { + bindingInput = constructTokenBindingInput(ctx.req, bindingHeaders); + } catch (error) { + next(error); return; } - if (!isBindingValid(bindingValue, claims)) { - unauthorized(ctx, 'binding_mismatch', 'Invalid token binding.'); + if (!isBindingValid(bindingInput, claims)) { + next(createUnauthorizedError('binding_mismatch', 'Invalid token binding.')); return; } } @@ -284,25 +307,43 @@ function normalizeBindingHeaders(bindingHeaders) { .filter((header) => hasText(header)); } -function extractBindingValue(headers, bindingHeaders) { +function constructTokenBindingInput(req, bindingHeaders) { + if (!req || typeof req !== 'object' || !req.headers) { + throw createHttpError( + 400, + 'invalid_request', + 'A valid HTTP request object is required for token binding validation.', + { logSummary: 'invalid_request' } + ); + } + const values = []; for (const header of bindingHeaders) { - const value = trimOrNull(headerValue(headers, header)); + const value = trimOrNull(headerValue(req.headers, header)); if (!hasText(value)) { - return null; + throw createUnauthorizedError( + 'missing_binding_header', + `Missing binding header: ${header}.` + ); } values.push(value); } + + // Build a single string from header values in configured order. return values.join(''); } -function isBindingValid(bindingValue, claims) { +function isBindingValid(bindingInput, claims) { + if (typeof bindingInput !== 'string') { + return false; + } + const expected = typeof claims.pay === 'string' ? claims.pay.trim() : ''; if (!hasText(expected)) { return false; } - const computed = hashBase64(bindingValue); + const computed = generateTokenBindingHash(bindingInput); return safeStringEqual(expected, computed); } @@ -323,7 +364,7 @@ function initializeRequestState(ctx) { ctx.state.logSummary = defaultSummaryForRoute(ctx.route, ctx.state); ctx.res.on('finish', () => { const status = ctx.res.statusCode; - if (status === 200 || status === 401) { + if (status === 200 || status >= 400) { logRequestCompleted(ctx); } }); @@ -404,16 +445,47 @@ function headerValue(headers, name) { return Array.isArray(value) ? value[0] : value; } -function runMiddleware(ctx, middlewares, handler) { +function runMiddleware(ctx, middlewares, handler, done) { let index = -1; + const onComplete = typeof done === 'function' ? done : () => {}; + + const dispatch = (error) => { + if (error) { + onComplete(error); + return; + } - const dispatch = () => { index += 1; - if (index < middlewares.length) { - middlewares[index](ctx, dispatch); + if (index > middlewares.length) { + onComplete( + createHttpError( + 500, + 'server_error', + 'Middleware called next() more than once.', + { exposeMessage: false, logSummary: 'server_error' } + ) + ); return; } - handler(ctx); + + const current = + index < middlewares.length ? middlewares[index] : handler; + + try { + const result = + index < middlewares.length ? current(ctx, dispatch) : current(ctx); + + if (result instanceof Error) { + onComplete(result); + return; + } + + if (index >= middlewares.length) { + onComplete(); + } + } catch (caught) { + onComplete(caught); + } }; dispatch(); @@ -421,16 +493,74 @@ function runMiddleware(ctx, middlewares, handler) { function parseRequest(req) { const host = req.headers.host || `${SERVER_HOSTNAME}:${HTTP_PORT}`; - const url = new URL(req.url || '/', `http://${host}`); + let url; + try { + url = new URL(req.url || '/', `http://${host}`); + } catch (error) { + throw createHttpError(400, 'invalid_request', 'Invalid request URL.', { + logSummary: 'invalid_request', + }); + } return { path: url.pathname, method: (req.method || 'GET').toUpperCase(), }; } -function unauthorized(ctx, reason, message) { - ctx.state.logSummary = `approov_failed:${reason}`; - writeJson(ctx.res, 401, { error: 'unauthorized', message }); +function handleRequestError(ctx, error) { + const appError = normalizeRequestError(error); + if (hasText(appError.logSummary)) { + ctx.state.logSummary = appError.logSummary; + } + + if (ctx.res.headersSent) { + return; + } + + if (appError.statusCode >= 500) { + console.error('Unhandled error:', error); + } + + writeJson(ctx.res, appError.statusCode, { + error: appError.code, + message: appError.exposeMessage + ? appError.message + : 'Internal server error.', + }); +} + +function normalizeRequestError(error) { + if (error instanceof HttpError) { + return error; + } + + return createHttpError(500, 'server_error', 'Internal server error.', { + exposeMessage: false, + logSummary: 'server_error', + }); +} + +function createUnauthorizedError(reason, message) { + return createHttpError(401, 'unauthorized', message, { + logSummary: `approov_failed:${reason}`, + details: { reason }, + }); +} + +function createHttpError(statusCode, code, message, options) { + return new HttpError(statusCode, code, message, options); +} + +class HttpError extends Error { + constructor(statusCode, code, message, options = {}) { + super(message); + this.name = 'HttpError'; + this.statusCode = statusCode; + this.code = code; + this.details = options.details || null; + this.logSummary = options.logSummary || null; + this.exposeMessage = options.exposeMessage !== false; + } } function writeJson(res, statusCode, payload) { @@ -499,10 +629,14 @@ function signHmac(value, secret) { return crypto.createHmac('sha256', secret).update(value).digest(); } -function hashBase64(value) { +function generateTokenBindingHash(bindingInput) { + if (typeof bindingInput !== 'string') { + throw new TypeError('Token binding input must be a string.'); + } + return crypto .createHash('sha256') - .update(value, 'utf8') + .update(bindingInput, 'utf8') .digest('base64'); } diff --git a/Dockerfile b/Dockerfile index c737cd6..26a25da 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,6 @@ # syntax=docker/dockerfile:1 -# Builds the quickstart backend container image and configures scripts/install-prerequisites.sh and scripts/build.sh # as the entrypoint used both locally and when deployed via Docker. -FROM node:22-slim +FROM node:24-bookworm-slim ENV APP_HOME=/workspace \ RUN_MODE=container @@ -11,6 +10,5 @@ WORKDIR /app COPY . . RUN npm install - # Provide APP_START_CMD via --env-file. -CMD ["bash", "scripts/build.sh"] +CMD ["bash", "scripts/build.sh"] \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f26e193 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Approov Limited + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md index 03bedcb..7970889 100644 --- a/README.md +++ b/README.md @@ -9,21 +9,21 @@ This project provides a server-side example of Approov token verification for a In this example, Approov token check is implemented in `ApproovApplication.js`. The responsibilities break down as follows: -1. **JWT Approov Token validation (signature + expiry)** is implemented in [`verifyApproovToken`](https://github.com/approov/quickstart-nodejs-token-check/blob/refactor/nodejs-quickstart/ApproovApplication.js#L243-L257) and [`validateExpiration`](https://github.com/approov/quickstart-nodejs-token-check/blob/refactor/nodejs-quickstart/ApproovApplication.js#L309-L319). +1. **JWT Approov Token validation (signature + expiry)** is in [verifyApproovToken](https://github.com/approov/quickstart-nodejs-token-check/blob/refactor/nodejs-quickstart/ApproovApplication.js#L243-L257) and [validateExpiration](https://github.com/approov/quickstart-nodejs-token-check/blob/refactor/nodejs-quickstart/ApproovApplication.js#L309-L319). It verifies the HS256 signature and rejects tokens that are missing or past `exp`. -2. **Token binding (pay + hash)** is handled by [`isBindingValid`](https://github.com/approov/quickstart-nodejs-token-check/blob/refactor/nodejs-quickstart/ApproovApplication.js#L299-L307) and [`hashBase64`](https://github.com/approov/quickstart-nodejs-token-check/blob/refactor/nodejs-quickstart/ApproovApplication.js#L502-L507). +2. **Token binding (pay + hash)** is handled by [isBindingValid](https://github.com/approov/quickstart-nodejs-token-check/blob/refactor/nodejs-quickstart/ApproovApplication.js#L299-L307) and [generateTokenBindingHash](https://github.com/approov/quickstart-nodejs-token-check/blob/refactor/nodejs-quickstart/ApproovApplication.js#L502-L507). It computes `base64(sha256(binding_value))` and compares it to `pay` with a timing-safe check. -3. **Middleware enforcement** is done by [`approovTokenVerifier`](https://github.com/approov/quickstart-nodejs-token-check/blob/refactor/nodejs-quickstart/ApproovApplication.js#L200-L241). +3. **Middleware enforcement** is done by [approovTokenVerifier](https://github.com/approov/quickstart-nodejs-token-check/blob/refactor/nodejs-quickstart/ApproovApplication.js#L200-L241). Requests without valid token or binding are rejected with `401`. -4. **Binding value selection (what gets hashed)** is in [`normalizeBindingHeaders`](https://github.com/approov/quickstart-nodejs-token-check/blob/refactor/nodejs-quickstart/ApproovApplication.js#L278-L285) and [`extractBindingValue`](ApproovApplication.js#L287-L297). -It uses the headers configured on each protected route (currently `Authorization` for single binding, or `Authorization` + `SessionId` for double binding). +4. **Binding value selection (what gets hashed)** is in [normalizeBindingHeaders](https://github.com/approov/quickstart-nodejs-token-check/blob/refactor/nodejs-quickstart/ApproovApplication.js#L278-L285) and [constructTokenBindingInput](ApproovApplication.js#L287-L297). +It uses route-configured headers, currently `Authorization` for single binding, or `Authorization` + `SessionId` for double binding. -5. **Protected route requirements** are defined in the [`ROUTES` table](https://github.com/approov/quickstart-nodejs-token-check/blob/refactor/nodejs-quickstart/ApproovApplication.js#L26-L84) via the `requiresApproov` and `bindingHeaders` fields. +1. **Protected route requirements** are defined in the [ROUTES table](https://github.com/approov/quickstart-nodejs-token-check/blob/refactor/nodejs-quickstart/ApproovApplication.js#L26-L84) via `requiresApproov` and `bindingHeaders`. -6. **Protected routes are registered** in the [`ROUTE_TABLE` map](https://github.com/approov/quickstart-nodejs-token-check/blob/refactor/nodejs-quickstart/ApproovApplication.js#L86-L88) and resolved in the HTTP server handler [`createServer`](https://github.com/approov/quickstart-nodejs-token-check/blob/refactor/nodejs-quickstart/ApproovApplication.js#L92-L114). +2. **Protected routes are registered** in the [ROUTE_TABLE](https://github.com/approov/quickstart-nodejs-token-check/blob/refactor/nodejs-quickstart/ApproovApplication.js#L86-L88) and resolved in the `http.createServer` request handler [`createServer`](https://github.com/approov/quickstart-nodejs-token-check/blob/refactor/nodejs-quickstart/ApproovApplication.js#L92-L114). ## Approov Token Verification Flow @@ -240,7 +240,7 @@ curl -X GET http://localhost:8080/approov-state # check current state **Environments where the quickstart was tested:** ```text -* Runtime: node.js v25.2.1 +* Runtime: node.js v24.13.1 * Build Tool: npm 11.6.2 ``` diff --git a/package-lock.json b/package-lock.json index c51d547..14070fb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,7 @@ "name": "approov-node-token-check", "version": "1.0.0", "engines": { - "node": ">=18.0.0" + "node": ">=24.0.0" } } } diff --git a/package.json b/package.json index 3ceab8f..a7a0041 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,6 @@ "test": "bash test.sh" }, "engines": { - "node": ">=18.0.0" + "node": ">=24.0.0" } } diff --git a/scripts/build.sh b/scripts/build.sh index cf13e41..df10175 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -27,6 +27,45 @@ CONTAINER_NAME="${CONTAINER_NAME:-approov-quickstart-nodejs-app}" ENV_FILE="${ENV_FILE:-.env}" RUNTIME_BIN_DIR="${RUNTIME_BIN_DIR:-}" # optional runtime-specific bin path +trim_whitespace() { + local value="$1" + value="${value#"${value%%[![:space:]]*}"}" + value="${value%"${value##*[![:space:]]}"}" + printf '%s' "$value" +} + +strip_wrapping_quotes() { + local value="$1" + if [[ "$value" =~ ^\".*\"$ ]] || [[ "$value" =~ ^\'.*\'$ ]]; then + value="${value:1:-1}" + fi + printf '%s' "$value" +} + +validate_approov_secret_env() { + local env_file="$1" key="$2" placeholder="$3" + local raw_value + local secret_value + + if ! grep -Eq "^[[:space:]]*${key}=" "$env_file"; then + fail "${key} is missing in ${env_file}. Set ${key}= before running." + fi + + raw_value="$( + grep -E "^[[:space:]]*${key}=" "$env_file" | + tail -n 1 | + sed -E "s/^[[:space:]]*${key}=//" + )" + + secret_value="$(trim_whitespace "$raw_value")" + secret_value="$(strip_wrapping_quotes "$secret_value")" + secret_value="$(trim_whitespace "$secret_value")" + + if [[ -z "$secret_value" || "$secret_value" == "$placeholder" ]]; then + fail "${key} is not set. Please set ${key}= in ${env_file} before running." + fi +} + in_container() { [[ "$RUN_MODE" == "container" ]] || [[ -f "/.dockerenv" ]] } @@ -36,6 +75,10 @@ if in_container; then if [[ -n "$RUNTIME_BIN_DIR" ]]; then export PATH="${RUNTIME_BIN_DIR}:$PATH" # e.g., RUNTIME_BIN_DIR=/usr/local/go/bin to expose runtime binaries for golang fi + # Allow Docker env-file values that require quotes for dotenv parsers. + if [[ "$APP_START_CMD" =~ ^\".*\"$ ]] || [[ "$APP_START_CMD" =~ ^\'.*\'$ ]]; then + APP_START_CMD="${APP_START_CMD:1:-1}" + fi info "Container starting application: ${APP_START_CMD}" exec bash -c "$APP_START_CMD" fi @@ -47,6 +90,10 @@ fi [[ -f "$ENV_FILE" ]] || fail "$ENV_FILE not found. Run cp .env.example .env first." [[ -f Dockerfile ]] || fail "Dockerfile not found in $(pwd)" +validate_approov_secret_env \ + "$ENV_FILE" \ + "${APPROOV_SECRET_ENV:-APPROOV_BASE64URL_SECRET}" \ + "${APPROOV_SECRET_PLACEHOLDER:-approov_base64url_secret_here}" if docker ps -a --format '{{.Names}}' | grep -Fxq "$CONTAINER_NAME"; then info "Removing stale container ${CONTAINER_NAME}" @@ -68,9 +115,15 @@ wait_for_service() { local url="$1" timeout="$2" interval="$3" elapsed=0 info "Waiting for application to become ready at ${url}" until curl -fsS "$url" >/dev/null 2>&1; do + if ! docker ps --format '{{.Names}}' | grep -Fxq "$CONTAINER_NAME"; then + docker logs --tail 200 "$CONTAINER_NAME" >&2 || true + fail "Container ${CONTAINER_NAME} exited before becoming ready." + fi + sleep "$interval" elapsed=$((elapsed + interval)) if (( elapsed >= timeout )); then + docker logs --tail 200 "$CONTAINER_NAME" >&2 || true fail "Application did not become ready within ${timeout}s (last url: ${url})" fi done @@ -84,4 +137,4 @@ if [[ "$FOLLOW_LOGS" == "true" ]]; then docker logs -f "$CONTAINER_NAME" else info "Skipping container logs attachment." -fi +fi \ No newline at end of file From a0e8ae37fddb087f58ef9dd670c2bfc6c063dcdf Mon Sep 17 00:00:00 2001 From: kmilejMAC Date: Fri, 20 Feb 2026 15:37:12 +0000 Subject: [PATCH 7/8] readme fix --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7970889..055f834 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ It uses route-configured headers, currently `Authorization` for single binding, 1. **Protected route requirements** are defined in the [ROUTES table](https://github.com/approov/quickstart-nodejs-token-check/blob/refactor/nodejs-quickstart/ApproovApplication.js#L26-L84) via `requiresApproov` and `bindingHeaders`. -2. **Protected routes are registered** in the [ROUTE_TABLE](https://github.com/approov/quickstart-nodejs-token-check/blob/refactor/nodejs-quickstart/ApproovApplication.js#L86-L88) and resolved in the `http.createServer` request handler [`createServer`](https://github.com/approov/quickstart-nodejs-token-check/blob/refactor/nodejs-quickstart/ApproovApplication.js#L92-L114). +2. **Protected routes are registered** in the [ROUTE_TABLE](https://github.com/approov/quickstart-nodejs-token-check/blob/refactor/nodejs-quickstart/ApproovApplication.js#L86-L88) and resolved by the `http.createServer` request handler. ## Approov Token Verification Flow From 3bc4d83879d1204edf16954aeeb38ef4e3a6122b Mon Sep 17 00:00:00 2001 From: kmilejMAC Date: Fri, 20 Feb 2026 15:54:26 +0000 Subject: [PATCH 8/8] readme fix --- README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 055f834..a7c751b 100644 --- a/README.md +++ b/README.md @@ -9,21 +9,21 @@ This project provides a server-side example of Approov token verification for a In this example, Approov token check is implemented in `ApproovApplication.js`. The responsibilities break down as follows: -1. **JWT Approov Token validation (signature + expiry)** is in [verifyApproovToken](https://github.com/approov/quickstart-nodejs-token-check/blob/refactor/nodejs-quickstart/ApproovApplication.js#L243-L257) and [validateExpiration](https://github.com/approov/quickstart-nodejs-token-check/blob/refactor/nodejs-quickstart/ApproovApplication.js#L309-L319). +1. **JWT Approov Token validation (signature + expiry)** is in [verifyApproovToken](https://github.com/approov/quickstart-nodejs-token-check/blob/refactor/nodejs-quickstart/ApproovApplication.js#L266-L280) and [validateExpiration](https://github.com/approov/quickstart-nodejs-token-check/blob/refactor/nodejs-quickstart/ApproovApplication.js#L350-L360). It verifies the HS256 signature and rejects tokens that are missing or past `exp`. -2. **Token binding (pay + hash)** is handled by [isBindingValid](https://github.com/approov/quickstart-nodejs-token-check/blob/refactor/nodejs-quickstart/ApproovApplication.js#L299-L307) and [generateTokenBindingHash](https://github.com/approov/quickstart-nodejs-token-check/blob/refactor/nodejs-quickstart/ApproovApplication.js#L502-L507). +2. **Token binding (pay + hash)** is handled by [isBindingValid](https://github.com/approov/quickstart-nodejs-token-check/blob/refactor/nodejs-quickstart/ApproovApplication.js#L336-L348) and [generateTokenBindingHash](https://github.com/approov/quickstart-nodejs-token-check/blob/refactor/nodejs-quickstart/ApproovApplication.js#L632-L641). It computes `base64(sha256(binding_value))` and compares it to `pay` with a timing-safe check. -3. **Middleware enforcement** is done by [approovTokenVerifier](https://github.com/approov/quickstart-nodejs-token-check/blob/refactor/nodejs-quickstart/ApproovApplication.js#L200-L241). +3. **Middleware enforcement** is done by [approovTokenVerifier](https://github.com/approov/quickstart-nodejs-token-check/blob/refactor/nodejs-quickstart/ApproovApplication.js#L214-L264). Requests without valid token or binding are rejected with `401`. -4. **Binding value selection (what gets hashed)** is in [normalizeBindingHeaders](https://github.com/approov/quickstart-nodejs-token-check/blob/refactor/nodejs-quickstart/ApproovApplication.js#L278-L285) and [constructTokenBindingInput](ApproovApplication.js#L287-L297). +4. **Binding value selection (what gets hashed)** is in [normalizeBindingHeaders](https://github.com/approov/quickstart-nodejs-token-check/blob/refactor/nodejs-quickstart/ApproovApplication.js#L301-L308) and [constructTokenBindingInput](https://github.com/approov/quickstart-nodejs-token-check/blob/refactor/nodejs-quickstart/ApproovApplication.js#L310-L334). It uses route-configured headers, currently `Authorization` for single binding, or `Authorization` + `SessionId` for double binding. -1. **Protected route requirements** are defined in the [ROUTES table](https://github.com/approov/quickstart-nodejs-token-check/blob/refactor/nodejs-quickstart/ApproovApplication.js#L26-L84) via `requiresApproov` and `bindingHeaders`. +5. **Protected route requirements** are defined in the [ROUTES](https://github.com/approov/quickstart-nodejs-token-check/blob/refactor/nodejs-quickstart/ApproovApplication.js#L26-L84) via `requiresApproov` and `bindingHeaders`. -2. **Protected routes are registered** in the [ROUTE_TABLE](https://github.com/approov/quickstart-nodejs-token-check/blob/refactor/nodejs-quickstart/ApproovApplication.js#L86-L88) and resolved by the `http.createServer` request handler. +6. **Protected routes are registered** in the [ROUTE_TABLE](https://github.com/approov/quickstart-nodejs-token-check/blob/refactor/nodejs-quickstart/ApproovApplication.js#L86-L88) and resolved by the `http.createServer` request handler. ## Approov Token Verification Flow