diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..9310ebd --- /dev/null +++ b/.dockerignore @@ -0,0 +1,32 @@ +# Dependencies installed inside the container image. +node_modules +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* + +# Local environment files and secrets. +.env +.env.* +!.env.example + +# Local test artifacts generated by test.sh/Approov CLI usage. +.config/ +*.token +*.tok +*.time + +# VCS, editor, and OS metadata. +.git +.gitignore +.gitattributes +.DS_Store +.idea/ +.vscode/ + +# Files not needed by the runtime image. +README.md +LICENSE +test.sh +run-server.sh +Dockerfile diff --git a/.editorconfig b/.editorconfig deleted file mode 100644 index 8a48fcc..0000000 --- a/.editorconfig +++ /dev/null @@ -1,9 +0,0 @@ -# Editor configuration, see http://editorconfig.org -root = true - -[*] -charset = utf-8 -indent_style = space -indent_size = 2 -insert_final_newline = true -trim_trailing_whitespace = true 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 56a31c7..a363aa9 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,4 @@ node_modules/ .env packages/ !.gitkeep +.config/ diff --git a/ApproovApplication.js b/ApproovApplication.js new file mode 100644 index 0000000..c25ff2d --- /dev/null +++ b/ApproovApplication.js @@ -0,0 +1,430 @@ +'use strict'; + +const crypto = require('crypto'); +const express = require('express'); +const cors = require('cors'); +const jwt = require('jsonwebtoken'); +const dotenv = require('dotenv'); + +const envResult = dotenv.config({ quiet: true }); +if (envResult.error && envResult.error.code !== 'ENOENT') { + throw new Error(`Failed to load .env: ${envResult.error.message}`); +} + +const PORT = parsePort(process.env.HTTP_PORT, 8080); +const APPROOV_HEADER = 'Approov-Token'; +const AUTH_HEADER = 'Authorization'; +const SESSION_ID_HEADER = 'SessionId'; +const REQUIRED_SECRET_PLACEHOLDER = 'approov_base64url_secret_here'; +const APPROOV_SECRET = loadApproovSecret(); + +let approovEnabled = true; +let tokenBindingEnabled = true; + +// Approov-protected endpoints that require token checks. +const PROTECTED_PATHS = new Set([ + '/token-check', + '/token-binding', + '/token-double-binding', +]); + +const APPROOV_ERROR_CODES = { + MISSING_TOKEN: 'missing_approov_token', + TOKEN_VERIFICATION_FAILED: 'token_verification_failed', + TOKEN_MISSING_EXPIRATION: 'token_missing_expiration', + TOKEN_EXPIRED: 'token_expired', + MISSING_BINDING_HEADER: 'missing_binding_header', + BINDING_MISMATCH: 'binding_mismatch', +}; + +const app = express(); +app.disable('x-powered-by'); +app.enable('strict routing'); +app.enable('case sensitive routing'); +app.use(cors()); + +app.use(requestLoggingMiddleware); +app.use(approovAuthMiddleware); + +app.get('/', (req, res) => { + res.json(infoPayload(`Approov demo API is running on port ${PORT}.`)); +}); + +app.get('/approov-state', (req, res) => { + res.json(statePayload()); +}); + +app.post('/approov/enable', (req, res) => { + enableApproov(); + res.json(statePayload()); +}); + +app.post('/approov/disable', (req, res) => { + disableApproov(); + res.json(statePayload()); +}); + +app.post('/token-binding/enable', (req, res) => { + tokenBindingEnabled = true; + res.json(statePayload()); +}); + +app.post('/token-binding/disable', (req, res) => { + tokenBindingEnabled = false; + res.json(statePayload()); +}); + +app.get('/unprotected', (req, res) => { + res.json(infoPayload("Unprotected endpoint '/unprotected'; no Approov checks performed.")); +}); + +app.get('/token-check', (req, res) => { + res.json(infoPayload("Protected endpoint '/token-check'; Approov token verified.")); +}); + +app.get('/token-binding', (req, res) => { + const authorization = req.get(AUTH_HEADER); + const payload = infoPayload("Protected endpoint '/token-binding'; Approov token binding enforced."); + payload.authorizationHeaderPresent = hasText(authorization); + res.json(payload); +}); + +app.get('/token-double-binding', (req, res) => { + const authorization = req.get(AUTH_HEADER); + const sessionId = req.get(SESSION_ID_HEADER); + const payload = infoPayload("Protected endpoint '/token-double-binding'; dual token binding enforced."); + payload.authorizationHeaderPresent = hasText(authorization); + payload.sessionIdHeaderPresent = hasText(sessionId); + res.json(payload); +}); + +app.use(errorHandlingMiddleware); + +const server = app.listen(PORT, () => { + console.log(`Approov demo API listening on port ${PORT}.`); +}); + +process.on('SIGTERM', () => shutdown('SIGTERM')); +process.on('SIGINT', () => shutdown('SIGINT')); + +function shutdown(signal) { + console.log(`Received ${signal}, shutting down.`); + server.close(() => process.exit(0)); +} + +// Middleware that enforces Approov token checks on protected endpoints. +function approovAuthMiddleware(req, res, next) { + if (req.method === 'OPTIONS') { + return next(); + } + + if (!PROTECTED_PATHS.has(req.path)) { + req.approovSummary = 'approov_not_required'; + req.approovRequiredHeaders = []; + return next(); + } + + req.approovRequiredHeaders = requiredHeadersFor(req.path, approovEnabled, tokenBindingEnabled); + + if (!approovEnabled) { + req.approovSummary = 'approov_disabled'; + return next(); + } + + try { + const rawToken = trimOrNull(req.get(APPROOV_HEADER)); + const claims = verifyApproovToken(rawToken); + + if (tokenBindingEnabled) { + const bindingHeaders = bindingHeadersFor(req.path); + if (bindingHeaders.length > 0) { + const bindingValue = extractBindingValue(req, bindingHeaders); + if (!hasText(bindingValue)) { + throw new ApproovAuthError( + APPROOV_ERROR_CODES.MISSING_BINDING_HEADER, + 'Missing binding header value.' + ); + } + if (!isBindingValid(bindingValue, claims)) { + throw new ApproovAuthError( + APPROOV_ERROR_CODES.BINDING_MISMATCH, + 'Approov token binding mismatch.' + ); + } + } + } + + req.approovSummary = 'approov_ok'; + return next(); + } catch (err) { + setApproovFailureSummary(req, err); + logAuthFailure(err); + return next(authErrorFor(err)); + } +} + +// Token validation logic: signature check, expiration, and binding (when enabled). +function verifyApproovToken(token) { + if (!hasText(token)) { + throw new ApproovAuthError(APPROOV_ERROR_CODES.MISSING_TOKEN, 'Approov token missing.'); + } + + let claims; + try { + claims = jwt.verify(token, APPROOV_SECRET, { + algorithms: ['HS256'], + ignoreExpiration: true, + }); + } catch (err) { + throw new ApproovAuthError( + APPROOV_ERROR_CODES.TOKEN_VERIFICATION_FAILED, + 'Approov token verification failed.' + ); + } + + const exp = Number(claims.exp); + if (!Number.isFinite(exp)) { + throw new ApproovAuthError( + APPROOV_ERROR_CODES.TOKEN_MISSING_EXPIRATION, + 'Approov token missing expiration.' + ); + } + if (exp * 1000 <= Date.now()) { + throw new ApproovAuthError(APPROOV_ERROR_CODES.TOKEN_EXPIRED, 'Approov token expired.'); + } + + return claims; +} + +function isBindingValid(bindingValue, claims) { + const expected = typeof claims.pay === 'string' ? claims.pay.trim() : ''; + if (!hasText(expected)) { + return false; + } + + const computed = hashBase64(bindingValue); + return timingSafeEquals(expected, computed); +} + +function extractBindingValue(req, bindingHeaders) { + if (bindingHeaders.length === 0) { + return null; + } + + const values = []; + for (const header of bindingHeaders) { + const value = trimOrNull(req.get(header)); + if (!hasText(value)) { + return null; + } + values.push(value); + } + + if (values.length === 0) { + return null; + } + + return values.join(''); +} + +function bindingHeadersFor(path) { + if (path === '/token-binding') { + return [AUTH_HEADER]; + } + if (path === '/token-double-binding') { + return [AUTH_HEADER, SESSION_ID_HEADER]; + } + return []; +} + +function requiredHeadersFor(path, approovState, bindingState) { + if (!PROTECTED_PATHS.has(path) || !approovState) { + return []; + } + + const headers = [APPROOV_HEADER]; + if (bindingState) { + headers.push(...bindingHeadersFor(path)); + } + return headers; +} + +function enableApproov() { + approovEnabled = true; + tokenBindingEnabled = true; +} + +function disableApproov() { + approovEnabled = false; + tokenBindingEnabled = false; +} + +function statePayload() { + return { + approovEnabled, + tokenBindingEnabled, + }; +} + +function infoPayload(details) { + return { + ...statePayload(), + details, + }; +} + +function loadApproovSecret() { + const raw = process.env.APPROOV_BASE64URL_SECRET; + if (!hasText(raw) || raw.trim() === REQUIRED_SECRET_PLACEHOLDER) { + console.error('[Approov] Required secret is not set'); + throw new Error('APPROOV_BASE64URL_SECRET environment variable is not set.'); + } + + const encoded = raw.trim(); + try { + return decodeBase64UrlSecret(encoded); + } catch (err) { + console.error('[Approov] Required secret is invalid'); + throw new Error('APPROOV_BASE64URL_SECRET must be a non-empty, valid base64url string.'); + } +} + +function decodeBase64UrlSecret(encoded) { + if (!/^[A-Za-z0-9_-]+={0,2}$/.test(encoded)) { + throw new Error('APPROOV_BASE64URL_SECRET contains invalid characters.'); + } + + const decoded = Buffer.from(encoded, 'base64url'); + if (decoded.length === 0) { + throw new Error('APPROOV_BASE64URL_SECRET decoded to an empty value.'); + } + + const normalizedInput = encoded.replace(/=+$/, ''); + const canonical = decoded.toString('base64url'); + if (canonical !== normalizedInput) { + throw new Error('APPROOV_BASE64URL_SECRET is not canonical base64url.'); + } + + return decoded; +} + +function hashBase64(value) { + return crypto.createHash('sha256').update(value, 'utf8').digest('base64'); +} + +function timingSafeEquals(expected, actual) { + const expectedBuffer = Buffer.from(expected, 'utf8'); + const actualBuffer = Buffer.from(actual, 'utf8'); + if (expectedBuffer.length !== actualBuffer.length) { + return false; + } + return crypto.timingSafeEqual(expectedBuffer, actualBuffer); +} + +function logAuthFailure(err) { + if (err instanceof ApproovAuthError) { + console.warn(`[Approov] ${err.code}: ${err.message}`); + return; + } + console.warn('[Approov] Unexpected authentication error.', err); +} + +function authErrorFor(err) { + if (err instanceof ApproovAuthError) { + return new HttpError(401, err.code, 'Unauthorized.'); + } + return new HttpError(401, 'unexpected_auth_error', 'Unauthorized.'); +} + +function errorHandlingMiddleware(err, req, res, next) { + if (res.headersSent) { + return next(err); + } + + if (err instanceof HttpError) { + return res.status(err.status).json({ error: err.code }); + } + + console.error('[Approov] Unhandled server error.', err); + return res.status(500).json({ error: 'internal_server_error' }); +} + +function requestLoggingMiddleware(req, res, next) { + res.on('finish', () => { + if (res.statusCode !== 200 && res.statusCode !== 401) { + return; + } + + const summary = typeof req.approovSummary === 'string' + ? req.approovSummary + : res.statusCode === 401 + ? 'approov_failed:unauthorized' + : 'request_completed'; + const requiredHeaders = Array.isArray(req.approovRequiredHeaders) + ? req.approovRequiredHeaders + : requiredHeadersFor(req.path, approovEnabled, tokenBindingEnabled); + logRequestCompleted(req, res, summary, requiredHeaders); + }); + + next(); +} + +function logRequestCompleted(req, res, summary, requiredHeaders) { + const payload = { + summary, + method: req.method, + path: req.path, + status: res.statusCode, + ip: req.ip, + port: req.socket?.localPort ?? PORT, + approovEnabled, + tokenBindingEnabled, + required_headers: requiredHeaders, + }; + console.log(`[${formatTimestamp(new Date())}] http.request.completed ${JSON.stringify(payload)}`); +} + +function formatTimestamp(date) { + const pad = (value) => String(value).padStart(2, '0'); + return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad( + date.getHours() + )}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`; +} + +function setApproovFailureSummary(req, err) { + if (err instanceof ApproovAuthError && hasText(err.code)) { + req.approovSummary = `approov_failed:${err.code}`; + return; + } + req.approovSummary = 'approov_failed:unexpected_error'; +} + +function hasText(value) { + return typeof value === 'string' && value.trim() !== ''; +} + +function trimOrNull(value) { + return typeof value === 'string' ? value.trim() : null; +} + +function parsePort(value, fallback) { + const parsed = Number.parseInt(value, 10); + return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback; +} + +class ApproovAuthError extends Error { + constructor(code, message) { + super(message); + this.name = 'ApproovAuthError'; + this.code = code; + } +} + +class HttpError extends Error { + constructor(status, code, message) { + super(message); + this.name = 'HttpError'; + this.status = status; + this.code = code; + } +} diff --git a/Dockerfile b/Dockerfile index af659ec..6dd4662 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,47 +1,15 @@ -ARG TAG=17.7.1-slim +# syntax=docker/dockerfile:1 +# as the entrypoint used both locally and when deployed via Docker. +FROM node:24-bookworm-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 1eb1224..0000000 --- a/EXAMPLES.md +++ /dev/null @@ -1,113 +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](./servers/hello/src/unprotected-server) -* [Approov Protected Server - Token Check](./servers/hello/src/approov-protected-server/token-check) -* [Approov Protected Server - Token Binding Check](./servers/hello/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 ./servers/hello/src/approov-protected-server/token-check/.env.example ./servers/hello/src/approov-protected-server/token-check/.env -cp ./servers/hello/src/approov-protected-server/token-binding-check/.env.example ./servers/hello/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/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/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 e18fdfd..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](/servers/hello/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 servers/hello/src/unprotected-server/hello-server-unprotected.js servers/hello/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 servers/hello/src/unprotected-server/hello-server-unprotected.js servers/hello/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 servers/hello/src/approov-protected-server/token-check/hello-server-protected.js servers/hello/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-express-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 532d161..f9c08a1 100644 --- a/README.md +++ b/README.md @@ -1,151 +1,259 @@ -# Approov QuickStart - NodeJS Express Token Check +# Approov Backend Quickstart - Node.js Express -[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 with the NodeJS Express framework in a simple Hello API server, 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. -Originally this repo was just to show the Approov token integration example on a NodeJS Express API as described in the article: [Approov Integration in a NodeJS Express API](https://approov.io/blog//approov-integration-in-a-nodejs-express-api), that you can still find at [/servers/shapes-api](/servers/shapes-api). +In this example, Approov token checks are 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-express-token-check/blob/refactor/nodejs-express-quickstart/ApproovApplication.js#L167-L197). +It validates the JWT with `jwt.verify` (HS256) and rejects tokens that are missing or past `exp`. -## Approov Integration Quickstart +1. **Token binding (`pay` + hash)** is handled by [isBindingValid](https://github.com/approov/quickstart-nodejs-express-token-check/blob/refactor/nodejs-express-quickstart/ApproovApplication.js#L199-L207) and [hashBase64](https://github.com/approov/quickstart-nodejs-express-token-check/blob/refactor/nodejs-express-quickstart/ApproovApplication.js#L311-L313). +It computes `base64(sha256(binding_value))` and compares it to `pay` using `timingSafeEquals`. -The quickstart was tested with the following Operating Systems: +1. **Middleware enforcement** is done by [approovAuthMiddleware](https://github.com/approov/quickstart-nodejs-express-token-check/blob/refactor/nodejs-express-quickstart/ApproovApplication.js#L116-L164). +When Approov checks are enabled for a protected path, requests without valid token/binding are rejected with `401`. -* Ubuntu 20.04 -* MacOS Big Sur -* Windows 10 WSL2 - Ubuntu 20.04 +1. **Binding value selection (what gets hashed)** is in [extractBindingValue + bindingHeadersFor](https://github.com/approov/quickstart-nodejs-express-token-check/blob/refactor/nodejs-express-quickstart/ApproovApplication.js#L209-L238). +It uses the headers configured in `bindingHeadersFor` (currently `Authorization` for single binding, or `Authorization` + `SessionId` for double binding). -First, setup the [Approov CLI](https://approov.io/docs/latest/approov-installation/index.html#initializing-the-approov-cli). +1. **Protected route requirements** are defined in [PROTECTED_PATHS](https://github.com/approov/quickstart-nodejs-express-token-check/blob/refactor/nodejs-express-quickstart/ApproovApplication.js#L25-L29) and [requiredHeadersFor](https://github.com/approov/quickstart-nodejs-express-token-check/blob/refactor/nodejs-express-quickstart/ApproovApplication.js#L240-L250). -Now, register the API domain for which Approov will issues tokens: +2. **Protected routes are registered** in the Express route declarations [app.get/app.post](https://github.com/approov/quickstart-nodejs-express-token-check/blob/refactor/nodejs-express-quickstart/ApproovApplication.js#L49-L99). + +## Approov Token Verification Flow + +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. + +2. **Token Attachment:** + The app attaches this token to every API request using the `Approov-Token` HTTP header. + +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.* + +### Automated and Manual Testing -Next, enable your Approov `admin` role with: +*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` + `SessionId`). +- Logs full request/response details to `.config/logs/.log`. + +#### *1. Unprotected Endpoint (No Approov)* -For the Windows powershell: +- 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 -set APPROOV_ROLE=admin:___YOUR_APPROOV_ACCOUNT_NAME_HERE___ +curl -iX GET http://localhost:8080/unprotected +``` + +The response will be `200 OK` for this request: +```text +HTTP/1.1 200 OK +Content-Type: application/json +Cache-Control: no-cache ``` -Now, get your Approov Secret with the [Approov CLI](https://approov.io/docs/latest/approov-installation/index.html#initializing-the-approov-cli): +#### *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 -approov secret -get base64 +approov token -genExample example.com ``` -Next, add the [Approov secret](https://approov.io/docs/latest/approov-usage-documentation/#account-secret-key-export) to your project `.env` file: +*Use the generated token in the `Approov-Token` header and `/token-check` endpoint.* -```env -APPROOV_BASE64_SECRET=approov_base64_secret_here +```bash +curl -iX GET http://localhost:8080/token-check \ + -H "Approov-Token: valid_approov_token_here" ``` -Now, add to your `package.json` file the [JWT dependency](https://github.com/auth0/express-jwt): +The response will be `200 OK` for this request: -```json -"express-jwt": "^8.3.0" +```text +HTTP/1.1 200 OK +Content-Type: application/json +Cache-Control: no-cache ``` -Next, in your code require the JWT package: +*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:* -```javascript -const { expressjwt: jwt } = require('express-jwt') +```bash +approov token -setDataHashInToken ExampleAuthToken== -genExample example.com ``` -Now, grab the Approov secret and set it into a constant: +*Use the generated token with binding in the Approov-Token and Authorization headers when calling the /token-binding endpoint.* -```javascript -const dotenv = require('dotenv').config() -const approovBase64Secret = dotenv.parsed.APPROOV_BASE64_SECRET; -const approovSecret = Buffer.from(approovBase64Secret, 'base64') +```bash +curl -iX GET http://localhost:8080/token-binding \ + -H "Approov-Token: valid_approov_token_here" \ + -H "Authorization: ExampleAuthToken==" ``` -Next, verify the Approov token: - -```javascript -// Callback that performs the Approov token check using the express-jwt library -const verifyApproovToken = jwt({ - secret: APPROOV_SECRET, - requestProperty: 'approovTokenDecoded', - getToken: function fromApproovTokenHeader(req, res) { - return req.get('Approov-Token') - }, - algorithms: ['HS256'] -}) +The response will be `200 OK` for this request: + +```text +HTTP/1.1 200 OK +Content-Type: application/json +Cache-Control: no-cache ``` -Now, handle errors when verifying Approov tokens: - -```js -// Callback to handle the errors occurred while checking the Approov token. -const approovTokenErrorHandler = (err, req, res, next) => { - // When has an error, it means the header `Approov-Token` is empty, missing or - // have failed validation of signature, expire time or is malformed. - if (err.name === 'UnauthorizedError') { - res.status(401) - res.json({}) - return - } - - next() -} +*If you use an invalid or missing header or token, the server will respond with `401 Unauthorized`.* + +#### Approov Token Binding Check with Two Different Bound Values + +- The client sends three headers on authenticated API calls: + - `Approov-Token` + - `Authorization` + - `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 `SessionId` headers:* + +```bash +approov token -setDataHashInToken ExampleAuthToken==123 -genExample example.com ``` -Next, set the callbacks as a request middleware: +*Use the generated token with two bindings in the Approov-Token and Authorization headers when calling the `/token-double-binding` endpoint.* -```js -const api = express() +```bash +curl -iX GET http://localhost:8080/token-double-binding \ + -H "Approov-Token: valid_approov_token_here" \ + -H "Authorization: ExampleAuthToken==" \ + -H "SessionId: 123" +``` -// Middleware to handle the validation of the Approov token for all your API -// endpoints. -api.use(verifyApproovToken) -api.use(approovTokenErrorHandler) -```` +The response will be `200 OK` for this request. -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. +```text +HTTP/1.1 200 OK +Content-Type: application/json +Cache-Control: no-cache +``` +*If you use an invalid or missing header or token, the server will respond with `401 Unauthorized`.* -## More Information +## Enable or Disable Approov Protection -* [Approov Overview](OVERVIEW.md) -* [Detailed Quickstarts](QUICKSTARTS.md) -* [Examples](EXAMPLES.md) -* [Testing](TESTING.md) +When the example server is running on `localhost:8080`, you can toggle Approov protection with these commands: -### System Clock +```bash +curl -X POST http://localhost:8080/approov/disable # disable 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 POST http://localhost:8080/approov/enable # enable the Approov service +curl -X GET http://localhost:8080/approov-state # check current state +``` -## Issues +*You can rerun the tests with Approov disabled to observe how the application behaves when the Approov protection is ***no longer active***.* -If you find any issue while following our instructions then just report it [here](https://github.com/approov/quickstart-nodejs-express-token-check/issues), with the steps to reproduce it, and we will sort it out and/or guide you to the correct path. +## Reporting Issues +**Environments where the quickstart was tested:** +```text +* Runtime: Node.js v24.13.1 +* Framework: Express 5.2.1 +* Build Tool: npm 11.6.2 +``` -## Useful Links +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-express-token-check/issues) and we will be happy to assist you. -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: +## Useful Links -* [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) diff --git a/TESTING.md b/TESTING.md deleted file mode 100644 index f92a42f..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-express-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 769f610..0000000 --- a/docker-compose.yml +++ /dev/null @@ -1,43 +0,0 @@ -version: "2.3" - -services: - - unprotected-server: - image: approov/nodejs-express:17.7.1 - build: ./ - environment: - - DEBUG=hello-server - networks: - - default - command: sh -c "npm install && npm start" - ports: - - ${HOST_IP:-127.0.0.1}:${HTTP_PORT:-8002}:${HTTP_PORT:-8002} - volumes: - - ./servers/hello/src/unprotected-server:/home/node/workspace - - approov-token-check: - image: approov/nodejs-express:17.7.1 - build: ./ - environment: - - DEBUG=hello-server - networks: - - default - command: sh -c "npm install && npm start" - ports: - - ${HOST_IP:-127.0.0.1}:${HTTP_PORT:-8002}:${HTTP_PORT:-8002} - volumes: - - ./servers/hello/src/approov-protected-server/token-check:/home/node/workspace - - approov-token-binding-check: - image: approov/nodejs-express:17.7.1 - build: ./ - environment: - - DEBUG=hello-server - networks: - - default - command: sh -c "npm install && npm start" - ports: - - ${HOST_IP:-127.0.0.1}:${HTTP_PORT:-8002}:${HTTP_PORT:-8002} - volumes: - - ./servers/hello/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 d546ea9..0000000 --- a/docs/APPROOV_TOKEN_BINDING_QUICKSTART.md +++ /dev/null @@ -1,376 +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 Express 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 your 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 [/servers/hello/src/approov-protected-server/token-binding-check/hello_server_protected.js](/servers/hello/src/approov-protected-server/token-binding-check/hello_server_protected.js). Take a look at the `_verifyApproovToken()` and `_erifyApproovTokenBinding()` functions to see the simple code for the checks. - -[TOC](#toc---table-of-contents) - - -## Requirements - -To complete this quickstart you will need both NodeJS and the Approov CLI tool installed. - -* [NodeJS](https://nodejs.org/en/download/) -* [Approov CLI](https://approov.io/docs/latest/approov-installation/#approov-tool) - Learn how to use it [here](https://approov.io/docs/latest/approov-cli-tool-reference/) - -[TOC](#toc---table-of-contents) - - -## Approov Setup - -To use Approov with the NodeJS Express API server we need a small amount of configuration. First, Approov needs to know the API domain that will be protected. Second, the NodeJS Express 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 Express 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. - -First, enable your Approov `admin` role with: - -```bash -eval `approov role admin` -```` - -For the Windows powershell: - -```bash -set APPROOV_ROLE=admin:___YOUR_APPROOV_ACCOUNT_NAME_HERE___ -``` - -Next, 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/express-jwt](https://github.com/auth0/express-jwt) package, but you are free to use another one of your preference. - -Below is [this working server](/servers/hello/src/approov-protected-server/token-binding-check) with the Approov token check. Add to your project the code from the sections `DEPENDENCIES`, `LOAD ENV VARS`, `APPROOV CALLBACKS`, `MIDDLEWARE` and `HELPERS`. - -```javascript -const debug = require('debug')('hello-server') -const express = require('express') -const cors = require('cors') -const api = express() -api.use(cors()) - - -////////////////// -// DEPENDENCIES -////////////////// - -const dotenv = require('dotenv').config() -const { expressjwt: jwt } = require('express-jwt') -const crypto = require('crypto') - - -/////////////////// -// LOAD ENV VARS -/////////////////// - -if (dotenv.error) { - throw new Error('LOAD ENV VARS: ' + dotenv.error) -} - -if (! "APPROOV_BASE64_SECRET" in dotenv.parsed) { - throw new Error("LOAD ENV VARS: Failed to load APPROOV_BASE64_SECRET. Check it's set in the .env file") -} - -const APPROOV_SECRET = Buffer.from(dotenv.parsed.APPROOV_BASE64_SECRET, 'base64') - - -/////////////////////// -// APPROOV CALLBACKS -/////////////////////// - -// Callback that performs the Approov token check using the express-jwt library -const verifyApproovToken = jwt({ - secret: APPROOV_SECRET, - requestProperty: 'approovTokenDecoded', - getToken: function fromApproovTokenHeader(req, res) { - return req.get('Approov-Token') - }, - algorithms: ['HS256'] -}) - -// Callback to handle the errors occurred while checking the Approov token. -const approovTokenErrorHandler = (err, req, res, next) => { - // When has an error, it means the header `Approov-Token` is empty, missing or - // have failed validation of signature, expire time or is malformed. - // @see verifyApproovToken() - if (err.name === 'UnauthorizedError') { - // You may want to enable logging here... - // debug("---> Approov token error -> " + err) - res.status(401) - res.json({}) - return - } - - next() -} - -// Callback to check the Approov token binding in the header matches with the -// one in the key `pay` of the Approov token claims. -const verifyApproovTokenBinding = (req, res, next) => { - - // The decoded Approov token was added to the request object when the checked - // it at `verifyApproovToken()` - token_binding_payload = req.approovTokenDecoded.pay - - if (token_binding_payload === undefined) { - // You may want to enable logging here... - // debug("---> Approov token binding error -> key 'pay' is missing in the claims of the Approov token payload.") - res.status(401) - res.json({}) - return - } - - if (isEmptyString(token_binding_payload)) { - // You may want to enable logging here... - // debug("---> Approov token binding error -> key 'pay' in the decoded token is empty.") - res.status(401) - res.json({}) - return - } - - // We use here the Authorization token, but feel free to use another header, - // but you need to bind this header to the Approov token in the mobile app. - const token_binding_header = req.get('Authorization') - - if (isEmptyString(token_binding_header)) { - // You may want to enable logging here... - // debug("---> Approov token binding error -> Missing or empty header to perform the verification for the token binding.") - res.status(401) - res.json({}) - return - } - - // We need to hash and base64 encode the token binding header, because that's - // how it was included in the Approov token payload claim. - const token_binding_header_encoded = crypto.createHash('sha256').update(token_binding_header, 'utf-8').digest('base64') - - if (token_binding_payload !== token_binding_header_encoded) { - // You may want to enable logging here... - // debug("---> Approov token error -> token binding in header doesn't match with the key 'pay' in the decoded token.") - res.status(401) - res.json({}) - return - } - - // Let the request continue as usual. - next() -} - - -//////////////// -// MIDDLEWARE -//////////////// - -api.use(logRequest) - -// Middleware to handle the validation of the Approov token. -api.use(verifyApproovToken) -api.use(approovTokenErrorHandler) -api.use(verifyApproovTokenBinding) - - -//////////////// -// ENDPOINTS -//////////////// - -// simple 'hello world' endpoint. -api.get('/', function (req, res, next) { - res.json({ - message: "Your API endpoints...", - }) -}) - - -//////////// -// SERVER -//////////// - -// Create and run the HTTP server -api.listen(8002, function () { - debug("Server listening on %s", "localhost") -}) - - -///////////// -// HELPERS -///////////// - -const isEmpty = function(value) { - return (value === undefined) || (value === null) || (value === '') -} - -const isString = function(value) { - return (typeof(value) === 'string') -} - -const isEmptyString = function(value) { - return (isEmpty(value) === true) || (isString(value) === false) || (value.trim() === '') -} - -``` - -> **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 [/servers/hello/src/approov-protected-server/token-binding-check](/servers/hello/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/1.1 200 OK - -... - -{"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/1.1 401 Unauthorized - -... - -{} -``` - -##### 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/1.1 401 Unauthorized - -... - -{} -``` - -[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-express-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/docs/APPROOV_TOKEN_QUICKSTART.md b/docs/APPROOV_TOKEN_QUICKSTART.md deleted file mode 100644 index e3f432e..0000000 --- a/docs/APPROOV_TOKEN_QUICKSTART.md +++ /dev/null @@ -1,284 +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 Express 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 your 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 Approov token check is in the file [/servers/hello/src/approov-protected-server/token-check/hello-server-protected.js](/servers/hello/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 NodeJS and the Approov CLI tool installed. - -* [NodeJS](https://nodejs.org/en/download/) -* [Approov CLI](https://approov.io/docs/latest/approov-installation/#approov-tool) - Learn how to use it [here](https://approov.io/docs/latest/approov-cli-tool-reference/) - -[TOC](#toc---table-of-contents) - - -## Approov Setup - -To use Approov with the NodeJS Express API server we need a small amount of configuration. First, Approov needs to know the API domain that will be protected. Second, the NodeJS Express 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 Express 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. - -First, enable your Approov `admin` role with: - -```bash -eval `approov role admin` -```` - -For the Windows powershell: - -```bash -set APPROOV_ROLE=admin:___YOUR_APPROOV_ACCOUNT_NAME_HERE___ -``` - -Next, 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/express-jwt](https://github.com/auth0/express-jwt) package, but you are free to use another one of your preference. - -Below is [this working server](/servers/hello/src/approov-protected-server/token-check) with the Approov token check. Add to your project the code from the sections `DEPENDENCIES`, `LOAD ENV VARS`, `APPROOV CALLBACKS` and `MIDDLEWARE`. - -```javascript -const debug = require('debug')('hello-server') -const express = require('express') -const cors = require('cors') -const api = express() -api.use(cors()) - - -////////////////// -// DEPENDENCIES -////////////////// - -const dotenv = require('dotenv').config() -const { expressjwt: jwt } = require('express-jwt') - - -/////////////////// -// LOAD ENV VARS -/////////////////// - -if (dotenv.error) { - throw new Error('LOAD ENV VARS: ' + dotenv.error) -} - -if (! "APPROOV_BASE64_SECRET" in dotenv.parsed) { - throw new Error("LOAD ENV VARS: Failed to load APPROOV_BASE64_SECRET. Check it's set in the .env file") -} - -const APPROOV_SECRET = Buffer.from(dotenv.parsed.APPROOV_BASE64_SECRET, 'base64') - - -/////////////////////// -// APPROOV CALLBACKS -/////////////////////// - -// Callback that performs the Approov token check using the express-jwt library -const verifyApproovToken = jwt({ - secret: APPROOV_SECRET, - requestProperty: 'approovTokenDecoded', - getToken: function fromApproovTokenHeader(req, res) { - return req.get('Approov-Token') - }, - algorithms: ['HS256'] -}) - -// Callback to handle the errors occurred while checking the Approov token. -const approovTokenErrorHandler = (err, req, res, next) => { - // When has an error, it means the header `Approov-Token` is empty, missing or - // have failed validation of signature, expire time or is malformed. - // @see verifyApproovToken() - if (err.name === 'UnauthorizedError') { - // You may want to enable logging here... - //debug("---> Approov token error -> " + err) - res.status(401) - res.json({}) - return - } - - next() -} - - -//////////////// -// MIDDLEWARE -//////////////// - -// Middleware to handle the validation of the Approov token. -api.use(verifyApproovToken) -api.use(approovTokenErrorHandler) - - -//////////////// -// ENDPOINTS -//////////////// - -// Your API endpoints ... -api.get('/', function (req, res, next) { - res.json({ - message: "Your API endpoints...", - }) -}) - - -//////////// -// SERVER -//////////// - -// Create and run the HTTP server -api.listen(8002, function () { - debug("Server listening on %s", "localhost") -} -``` - -> **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 [/servers/hello/src/approov-protected-server/token-check](/servers/hello/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/1.1 200 OK - -... - -{"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/1.1 401 Unauthorized - -... -{} -``` - -[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-express-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/package-lock.json b/package-lock.json new file mode 100644 index 0000000..0fafeb2 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,999 @@ +{ + "name": "approov-quickstart-node-express", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "approov-quickstart-node-express", + "version": "1.0.0", + "license": "Apache-2.0", + "dependencies": { + "cors": "^2.8.5", + "dotenv": "^17.2.3", + "express": "^5.2.1", + "jsonwebtoken": "^9.0.3" + }, + "engines": { + "node": ">=24 <25" + } + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/body-parser": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", + "integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "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==", + "license": "BSD-3-Clause" + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/content-disposition": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", + "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dotenv": { + "version": "17.2.3", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz", + "integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "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==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", + "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/jsonwebtoken": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", + "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", + "license": "MIT", + "dependencies": { + "jws": "^4.0.1", + "lodash.includes": "^4.3.0", + "lodash.isboolean": "^3.0.3", + "lodash.isinteger": "^4.0.4", + "lodash.isnumber": "^3.0.3", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.once": "^4.0.0", + "ms": "^2.1.1", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=12", + "npm": ">=6" + } + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/lodash.includes": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", + "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", + "license": "MIT" + }, + "node_modules/lodash.isboolean": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", + "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", + "license": "MIT" + }, + "node_modules/lodash.isinteger": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", + "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", + "license": "MIT" + }, + "node_modules/lodash.isnumber": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", + "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "license": "MIT" + }, + "node_modules/lodash.once": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", + "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", + "license": "MIT" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "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==", + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-to-regexp": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", + "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz", + "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==", + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "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" + } + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..6bb3ba2 --- /dev/null +++ b/package.json @@ -0,0 +1,20 @@ +{ + "name": "approov-quickstart-node-express", + "version": "1.0.0", + "description": "Approov token check quickstart for a Node.js Express backend.", + "main": "ApproovApplication.js", + "scripts": { + "start": "node ApproovApplication.js", + "dev": "node --watch-path=ApproovApplication.js ApproovApplication.js" + }, + "engines": { + "node": ">=24 <25" + }, + "license": "Apache-2.0", + "dependencies": { + "cors": "^2.8.5", + "dotenv": "^17.2.3", + "express": "^5.2.1", + "jsonwebtoken": "^9.0.3" + } +} 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..3677e4e --- /dev/null +++ b/scripts/build.sh @@ -0,0 +1,140 @@ +#!/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-express}" +CONTAINER_NAME="${CONTAINER_NAME:-approov-quickstart-nodejs-express-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" ]] +} + +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 + # 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 + +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)" +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}" + 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 + 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 + 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 \ No newline at end of file diff --git a/servers/hello/src/approov-protected-server/token-binding-check/.env.example b/servers/hello/src/approov-protected-server/token-binding-check/.env.example deleted file mode 100644 index c1ed6f2..0000000 --- a/servers/hello/src/approov-protected-server/token-binding-check/.env.example +++ /dev/null @@ -1,7 +0,0 @@ -# For production usage the secret is always retrieved with the Approov CLI tool, -# that can be also used to generate valid and invalid tokens for testing purposes. -# Please check the Approov docs at https://approov.io/docs/latest/approov-cli-tool-reference/#token-commands. -# -# For following along this Hello server examples you just need to use the dummy -# secret provided in the README.md#the-dummyd-secret at the root of this repo. -APPROOV_BASE64_SECRET=approov_base64_secret_here diff --git a/servers/hello/src/approov-protected-server/token-binding-check/README.md b/servers/hello/src/approov-protected-server/token-binding-check/README.md deleted file mode 100644 index c00324a..0000000 --- a/servers/hello/src/approov-protected-server/token-binding-check/README.md +++ /dev/null @@ -1,120 +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 Express API server. - - -## TOC - Table of Contents - -* [Why?](#why) -* [How it Works?](#how-it-works) -* [Requirements](#requirements) -* [Setup Env File](#setup-env-file) -* [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 Express API server is very simple and is defined in the file [src/approov-protected-server/token-binding-check/hello_server_protected.js](/servers/hello/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 installed: - -* [NodeJS](https://nodejs.org/en/download/) - -[TOC](#toc---table-of-contents) - - -## Setup Env File - -From `/servers/hello/src/approov-protected-server/token-check` execute the following: - -```bash -cp .env.example .env -``` - -Edit the `.env` 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). - -[TOC](#toc---table-of-contents) - - -## Try the Approov Integration Example - -First, you need to set the dummy secret in the `/servers/hello/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 `/servers/hello/src/approov-protected-server/token-check` folder execute: - -```bash -npm install -``` - -Now, you can run this example from the `/servers/hello/src/approov-protected-server/token-check` folder with: - -```text -npm start -``` - -Next, you can test that it works with: - -```text -curl -iX GET 'http://localhost:8002' -``` - -The response will be a `401` unauthorized request: - -```text -HTTP/1.1 401 Unauthorized -X-Powered-By: Express -Access-Control-Allow-Origin: * -Content-Type: application/json; charset=utf-8 -Content-Length: 2 -ETag: W/"2-vyGp6PvFo4RvsFtPoIWeCReyIC8" -Date: Wed, 16 Mar 2022 19:59:24 GMT -Connection: keep-alive -Keep-Alive: timeout=5 - -{} -``` - -The reason you got a `401` is because no 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-express-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/servers/hello/src/approov-protected-server/token-binding-check/hello-server-protected.js b/servers/hello/src/approov-protected-server/token-binding-check/hello-server-protected.js deleted file mode 100644 index 693f9e2..0000000 --- a/servers/hello/src/approov-protected-server/token-binding-check/hello-server-protected.js +++ /dev/null @@ -1,167 +0,0 @@ -const debug = require('debug')('hello-server') -const dotenv = require('dotenv').config() -const { expressjwt: jwt } = require('express-jwt') -const crypto = require('crypto') -const express = require('express') -const cors = require('cors') -const api = express() -api.use(cors()) - - -/////////////////// -// LOAD ENV VARS -/////////////////// - -if (dotenv.error) { - throw new Error('LOAD ENV VARS: ' + dotenv.error) -} - -if (! "APPROOV_BASE64_SECRET" in dotenv.parsed) { - throw new Error("LOAD ENV VARS: Failed to load APPROOV_BASE64_SECRET. Check it's set in the .env file") -} - -const APPROOV_SECRET = Buffer.from(dotenv.parsed.APPROOV_BASE64_SECRET, 'base64') - - -////////////////////// -// LOGGING CALLBACK -////////////////////// - -const logRequest = (req, res, next) => { - debug('<<< ' + req.method + ' ' + req.originalUrl) - - req.on('end', () => { - debug('>>> ' + res.statusCode + ' ' + req.method + ' ' + req.originalUrl) - }) - - next() -} - - -/////////////////////// -// APPROOV CALLBACKS -/////////////////////// - -// Callback that performs the Approov token check using the express-jwt library -const verifyApproovToken = jwt({ - secret: APPROOV_SECRET, - requestProperty: 'approovTokenDecoded', - getToken: function fromApproovTokenHeader(req, res) { - return req.get('Approov-Token') - }, - algorithms: ['HS256'] -}) - -// Callback to handle the errors occurred while checking the Approov token. -const approovTokenErrorHandler = (err, req, res, next) => { - // When has an error, it means the header `Approov-Token` is empty, missing or - // have failed validation of signature, expire time or is malformed. - // @see verifyApproovToken() - if (err.name === 'UnauthorizedError') { - debug("---> Approov token error -> " + err) - res.status(401) - res.json({}) - return - } - - next() -} - -// Callback to check the Approov token binding in the header matches with the -// one in the key `pay` of the Approov token claims. -const verifyApproovTokenBinding = (req, res, next) => { - - // The decoded Approov token was added to the request object when the checked - // it at `verifyApproovToken()` - token_binding_payload = req.approovTokenDecoded.pay - - if (token_binding_payload === undefined) { - debug("---> Approov token binding error -> key 'pay' is missing in the claims of the Approov token payload.") - res.status(401) - res.json({}) - return - } - - if (isEmptyString(token_binding_payload)) { - debug("---> Approov token binding error -> key 'pay' in the decoded token is empty.") - res.status(401) - res.json({}) - return - } - - // We use here the Authorization token, but feel free to use another header, - // but you need to bind this header to the Approov token in the mobile app. - const token_binding_header = req.get('Authorization') - - if (isEmptyString(token_binding_header)) { - debug("---> Approov token binding error -> Missing or empty header to perform the verification for the token binding.") - res.status(401) - res.json({}) - return - } - - // We need to hash and base64 encode the token binding header, because that's - // how it was included in the Approov token payload claim. - const token_binding_header_encoded = crypto.createHash('sha256').update(token_binding_header, 'utf-8').digest('base64') - - if (token_binding_payload !== token_binding_header_encoded) { - debug("---> Approov token error -> token binding in header doesn't match with the key 'pay' in the decoded token.") - res.status(401) - res.json({}) - return - } - - // Let the request continue as usual. - next() -} - - -//////////////// -// MIDDLEWARE -//////////////// - -api.use(logRequest) - -// Middleware to handle the validation of the Approov token. -api.use(verifyApproovToken) -api.use(approovTokenErrorHandler) -api.use(verifyApproovTokenBinding) - - -//////////////// -// ENDPOINTS -//////////////// - -// simple 'hello world' endpoint. -api.get('/', function (req, res, next) { - res.json({ - message: "Hello, World!", - }) -}) - - -//////////// -// SERVER -//////////// - -// Create and run the HTTP server -api.listen(8002, function () { - debug("Server listening on %s", "localhost") -}) - - -///////////// -// HELPERS -///////////// - -const isEmpty = function(value) { - return (value === undefined) || (value === null) || (value === '') -} - -const isString = function(value) { - return (typeof(value) === 'string') -} - -const isEmptyString = function(value) { - return (isEmpty(value) === true) || (isString(value) === false) || (value.trim() === '') -} diff --git a/servers/hello/src/approov-protected-server/token-binding-check/package.json b/servers/hello/src/approov-protected-server/token-binding-check/package.json deleted file mode 100644 index 111e774..0000000 --- a/servers/hello/src/approov-protected-server/token-binding-check/package.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "name": "demo-shapes-server", - "version": "1.0.0", - "description": "Node Express - Approov Protected Hello server.", - "main": "hello-server-protected.js", - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1", - "start": "nodemon hello-server-protected.js" - }, - "author": "CriticalBlue.com", - "license": "Apache-2.0", - "dependencies": { - "cors": "^2.8.5", - "debug": "^4.3.6", - "dotenv": "^16.4.5", - "express": "^4.19.2", - "express-jwt": "^8.4.1", - "fs": "0.0.1-security" - }, - "devDependencies": { - "minimist": "^1.2.8", - "nodemon": "^3.1.4" - } -} diff --git a/servers/hello/src/approov-protected-server/token-check/.env.example b/servers/hello/src/approov-protected-server/token-check/.env.example deleted file mode 100644 index c1ed6f2..0000000 --- a/servers/hello/src/approov-protected-server/token-check/.env.example +++ /dev/null @@ -1,7 +0,0 @@ -# For production usage the secret is always retrieved with the Approov CLI tool, -# that can be also used to generate valid and invalid tokens for testing purposes. -# Please check the Approov docs at https://approov.io/docs/latest/approov-cli-tool-reference/#token-commands. -# -# For following along this Hello server examples you just need to use the dummy -# secret provided in the README.md#the-dummyd-secret at the root of this repo. -APPROOV_BASE64_SECRET=approov_base64_secret_here diff --git a/servers/hello/src/approov-protected-server/token-check/README.md b/servers/hello/src/approov-protected-server/token-check/README.md deleted file mode 100644 index be4069f..0000000 --- a/servers/hello/src/approov-protected-server/token-check/README.md +++ /dev/null @@ -1,117 +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 Express API server. - -## TOC - Table of Contents - -* [Why?](#why) -* [How it Works?](#how-it-works) -* [Requirements](#requirements) -* [Setup Env File](#setup-env-file) -* [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 Express API server is very simple and is defined in the file [src/approov-protected-server/token-check/hello_server_protected.js](/servers/hello/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 installed: - -* [NodeJS](https://nodejs.org/en/download/) - -[TOC](#toc---table-of-contents) - - -## Setup Env File - -From `/servers/hello/src/approov-protected-server/token-check` execute the following: - -```bash -cp .env.example .env -``` - -Edit the `.env` 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). - -[TOC](#toc---table-of-contents) - - -## Try the Approov Integration Example - -First, you need to install the dependencies. From the `/servers/hello/src/approov-protected-server/token-check` folder execute: - -```bash -npm install -``` - -Now, you can run this example from the `/servers/hello/src/approov-protected-server/token-check` folder with: - -```text -npm start -``` - -Next, you can test that it works with: - -```text -curl -iX GET 'http://localhost:8002' -``` - -The response will be a `401` unauthorized request: - -```text -HTTP/1.1 401 Unauthorized -X-Powered-By: Express -Access-Control-Allow-Origin: * -Content-Type: application/json; charset=utf-8 -Content-Length: 2 -ETag: W/"2-vyGp6PvFo4RvsFtPoIWeCReyIC8" -Date: Wed, 16 Mar 2022 19:59:24 GMT -Connection: keep-alive -Keep-Alive: timeout=5 - -{} -``` - -The reason you got a `401` is because no 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-express-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/servers/hello/src/approov-protected-server/token-check/hello-server-protected.js b/servers/hello/src/approov-protected-server/token-check/hello-server-protected.js deleted file mode 100644 index ba57f7a..0000000 --- a/servers/hello/src/approov-protected-server/token-check/hello-server-protected.js +++ /dev/null @@ -1,100 +0,0 @@ -const debug = require('debug')('hello-server') -const dotenv = require('dotenv').config() -const { expressjwt: jwt } = require('express-jwt') -const express = require('express') -const cors = require('cors') -const api = express() -api.use(cors()) - - -/////////////////// -// LOAD ENV VARS -/////////////////// - -if (dotenv.error) { - throw new Error('LOAD ENV VARS: ' + dotenv.error) -} - -if (! "APPROOV_BASE64_SECRET" in dotenv.parsed) { - throw new Error("LOAD ENV VARS: Failed to load APPROOV_BASE64_SECRET. Check it's set in the .env file") -} - -const APPROOV_SECRET = Buffer.from(dotenv.parsed.APPROOV_BASE64_SECRET, 'base64') - - -////////////////////// -// LOGGING CALLBACK -////////////////////// - -const logRequest = (req, res, next) => { - debug('<<< ' + req.method + ' ' + req.originalUrl) - - req.on('end', () => { - debug('>>> ' + res.statusCode + ' ' + req.method + ' ' + req.originalUrl) - }) - - next() -} - - -/////////////////////// -// APPROOV CALLBACKS -/////////////////////// - -// Callback that performs the Approov token check using the express-jwt library -const verifyApproovToken = jwt({ - secret: APPROOV_SECRET, - requestProperty: 'approovTokenDecoded', - getToken: function fromApproovTokenHeader(req, res) { - return req.get('Approov-Token') - }, - algorithms: ['HS256'] -}) - -// Callback to handle the errors occurred while checking the Approov token. -const approovTokenErrorHandler = (err, req, res, next) => { - // When has an error, it means the header `Approov-Token` is empty, missing or - // have failed validation of signature, expire time or is malformed. - // @see verifyApproovToken() - if (err.name === 'UnauthorizedError') { - debug("---> Approov token error -> " + err) - res.status(401) - res.json({}) - return - } - - next() -} - - -//////////////// -// MIDDLEWARE -//////////////// - -api.use(logRequest) - -// Middleware to handle the validation of the Approov token. -api.use(verifyApproovToken) -api.use(approovTokenErrorHandler) - - -//////////////// -// ENDPOINTS -//////////////// - -// simple 'hello world' endpoint. -api.get('/', function (req, res, next) { - res.json({ - message: "Hello, World!", - }) -}) - - -//////////// -// SERVER -//////////// - -// Create and run the HTTP server -api.listen(8002, function () { - debug("Server listening on %s", "localhost") -}) diff --git a/servers/hello/src/approov-protected-server/token-check/package.json b/servers/hello/src/approov-protected-server/token-check/package.json deleted file mode 100644 index 111e774..0000000 --- a/servers/hello/src/approov-protected-server/token-check/package.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "name": "demo-shapes-server", - "version": "1.0.0", - "description": "Node Express - Approov Protected Hello server.", - "main": "hello-server-protected.js", - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1", - "start": "nodemon hello-server-protected.js" - }, - "author": "CriticalBlue.com", - "license": "Apache-2.0", - "dependencies": { - "cors": "^2.8.5", - "debug": "^4.3.6", - "dotenv": "^16.4.5", - "express": "^4.19.2", - "express-jwt": "^8.4.1", - "fs": "0.0.1-security" - }, - "devDependencies": { - "minimist": "^1.2.8", - "nodemon": "^3.1.4" - } -} diff --git a/servers/hello/src/unprotected-server/README.md b/servers/hello/src/unprotected-server/README.md deleted file mode 100644 index 1496703..0000000 --- a/servers/hello/src/unprotected-server/README.md +++ /dev/null @@ -1,96 +0,0 @@ -# Unprotected Server Example - -The unprotected example is the base reference to build the [Approov protected servers](/servers/hello/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](/servers/hello/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 Express API server is very simple and is defined in the file [src/unprotected-server/hello-server-unprotected.js](/servers/hello/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 installed: - -* [NodeJS](https://nodejs.org/en/download/) - -[TOC](#toc---table-of-contents) - - -## Try It - -First install the dependencies. - -From the `./servers/hello/src/unprotected-server` folder execute: - -```bash -npm install -``` - -Now, you can run this example from the `./servers/hello/src/unprotected-server` folder with: - -```bash -npm start -``` - -Finally, you can test that it works with: - -```bash -curl -X GET 'http://localhost:8002' -``` - -The response will be: - -```json -{"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-express-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/servers/hello/src/unprotected-server/hello-server-unprotected.js b/servers/hello/src/unprotected-server/hello-server-unprotected.js deleted file mode 100644 index 03804ee..0000000 --- a/servers/hello/src/unprotected-server/hello-server-unprotected.js +++ /dev/null @@ -1,49 +0,0 @@ -const debug = require('debug')('hello-server') -const express = require('express') -const cors = require('cors') -const api = express() -api.use(cors()) - - -////////////////////// -// LOGGING CALLBACK -////////////////////// - -const logRequest = (req, res, next) => { - debug('<<< ' + req.method + ' ' + req.originalUrl) - - req.on('end', () => { - debug('>>> ' + res.statusCode + ' ' + req.method + ' ' + req.originalUrl) - }) - - next() -} - - -//////////////// -// MIDDLEWARE -//////////////// - -api.use(logRequest) - - -//////////////// -// ENDPOINTS -//////////////// - -// simple 'hello world' endpoint. -api.get('/', function (req, res, next) { - res.json({ - message: "Hello, World!", - }) -}) - - -//////////// -// SERVER -//////////// - -// Create and run the HTTP server -api.listen(8002, function () { - debug("Server listening on %s", "localhost") -}) diff --git a/servers/hello/src/unprotected-server/package.json b/servers/hello/src/unprotected-server/package.json deleted file mode 100644 index e8162ee..0000000 --- a/servers/hello/src/unprotected-server/package.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "name": "demo-shapes-server", - "version": "1.0.0", - "description": "Node Express version of the Approov demo shapes server.", - "main": "hello-server-unprotected.js", - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1", - "start": "nodemon hello-server-unprotected.js" - }, - "author": "CriticalBlue.com", - "license": "Apache-2.0", - "dependencies": { - "cors": "^2.8.5", - "debug": "^4.3.6", - "dotenv": "^16.4.5", - "express": "^4.19.2", - "fs": "0.0.1-security" - }, - "devDependencies": { - "minimist": "^1.2.8", - "nodemon": "^3.1.4" - } -} diff --git a/servers/shapes-api/.env.example b/servers/shapes-api/.env.example deleted file mode 100644 index 1616196..0000000 --- a/servers/shapes-api/.env.example +++ /dev/null @@ -1,21 +0,0 @@ -################# -# NODE SERVER -################# - -ENV=production -HTTP_PROTOCOL=http -HTTP_PORT=8002 -DEBUG=approov-protected-server,original-server -SHAPES_NODEJS_EXPRESS_DOMAIN=nodejs-express.demo.approov.io - -############ -# APPROOV -############ - -# Feel free to play with different secrets. For development only you can create them with: -# openssl rand -base64 64 | tr -d '\n'; echo -APPROOV_BASE64_SECRET=approov-base64-encoded-secret-here - -APPROOV_ABORT_REQUEST_ON_INVALID_TOKEN=true -APPROOV_ABORT_REQUEST_ON_INVALID_TOKEN_BINDING=true -APPROOV_LOGGING_ENABLED=true diff --git a/servers/shapes-api/Dockerfile b/servers/shapes-api/Dockerfile deleted file mode 100644 index 23816c5..0000000 --- a/servers/shapes-api/Dockerfile +++ /dev/null @@ -1,54 +0,0 @@ -ARG TAG=18-slim -FROM node:${TAG} - -ARG CONTAINER_USER="node" -ARG LANGUAGE_CODE="en" -ARG COUNTRY_CODE="GB" -ARG ENCODING="UTF-8" - -ARG LOCALE_STRING="${LANGUAGE_CODE}_${COUNTRY_CODE}" -ARG LOCALIZATION="${LOCALE_STRING}.${ENCODING}" - -ARG OH_MY_ZSH_THEME="bira" - -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 && \ - npm install -g nodemon - -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}" - -RUN mkdir -p /home/${CONTAINER_USER}/workspace - -WORKDIR /home/${CONTAINER_USER}/workspace - -# Copy app source into the docker image with the correct ownership -COPY --chown=${CONTAINER_USER}:${CONTAINER_USER} approov-protected-server.js . -COPY --chown=${CONTAINER_USER}:${CONTAINER_USER} package.json . -COPY --chown=${CONTAINER_USER}:${CONTAINER_USER} package-lock.json . - -RUN pwd && ls -laR && npm install - -CMD ["zsh"] diff --git a/servers/shapes-api/README.md b/servers/shapes-api/README.md deleted file mode 100644 index b2eafc8..0000000 --- a/servers/shapes-api/README.md +++ /dev/null @@ -1,860 +0,0 @@ -# APPROOV INTEGRATION EXAMPLE - -An Approov token integration example for a NodeJS Express API as described in the article: [Approov Integration in a NodeJS Express API](https://approov.io/blog//approov-integration-in-a-nodejs-express-api). - -## HOW TO USE - -For your convenience we host ourselves the backend for this Approov integration walk-through, and the specific url for it can be found on the article, that we invite you to read in order to better understand the purpose and scope for this walk-through. - -If you prefer to have control of the backend please follow the [deployment](./docs/DEPLOYMENT.md) guide to deploy the backend to your own online server or just run it in localhost by following the [Approov Shapes API Server](./docs/approov-shapes-api-server.md) walk-through. - -The concrete implementation of the Approov Shapes API Server is in the -[approov-protected-server.js](./approov-protected-server.js) file, that -is a simple NodeJS Express server with some endpoints protected by Approov and -other endpoints without any Approov protection. - -Now let's continue reading this README for a **quickstart** introduction in how to integrate Approov on a current project by using as an example the code for the Approov Shapes API Server. - - -## APPROOV VALIDATION PROCESS - -Before we dive into the code we need to understand the Approov validation -process on the back-end side. - -### The Approov Token - -API calls protected by Approov will typically include a header holding an Approov -JWT token. This token must be checked to ensure it has not expired and that it is -properly signed with the secret shared between the back-end and the Approov cloud -service. - -We will use a NodeJS package to help us in the validation of the Approov JWT -token. - -> **NOTE** -> -> Just to be sure that we are on the same page, a JWT token have 3 parts, that -> are separated by dots and represented as a string in the format of -> `header.payload.signature`. Read more about JWT tokens [here](https://jwt.io/introduction/). - -### The Approov Token Binding - -When an Approov token contains the key `pay`, its value is a base64 encoded sha256 hash of -some unique identifier in the request, that we may want to bind with the Approov token, in order -to enhance the security on that request, like an Authorization token. - -Dummy example for the JWT token middle part, the payload: - -``` -{ - "exp": 123456789, # required - the timestamp for when the token expires. - "pay":"f3U2fniBJVE04Tdecj0d6orV9qT9t52TjfHxdUqDBgY=" # optional - a sha256 hash of the token binding value, encoded with base64. -} -``` - -The token binding in an Approov token is the one in the `pay` key: - -``` -"pay":"f3U2fniBJVE04Tdecj0d6orV9qT9t52TjfHxdUqDBgY=" -``` - -> **ALERT**: Please bear in mind that the token binding is not meant to pass application data to the API server. - -## SYSTEM CLOCK - -In order to correctly check for the expiration times of the Approov tokens is -very important that the NodeJS Express server is synchronizing automatically the -system clock over the network with an authoritative time source. In Linux this -is usual done with a NTP server. - - -## REQUIREMENTS - -We will use NodeJS 10 with an Express API server to run our code. - -Docker is required for the ones wanting to use the docker environment provided -by the [stack](./stack) bash script, that is a wrapper around docker commands. - -Postman is the tool we recommend to be used when simulating the queries against -the API, but feel free to use any other tool of your preference. - - -## THE DOCKER STACK - -We recommend the use of the included Docker stack to play with this Approov -integration. - -For details in how to use it you need to follow the setup instructions in the -[Approov Shapes API Server](./docs/approov-shapes-api-server.md#development-environment) -walk-through, but feel free to use your local environment to play with this -Approov integration. - - -## THE POSTMAN COLLECTION - -As you go through your Approov Integration you may want to test it and if you are using Postman then you can import this [Postman collection](https://raw.githubusercontent.com/approov/postman-collections/master/quickstarts/shapes-api/shapes-api.postman_collection.json) to see how it's done for the Approov Shapes API Server [example](./docs/approov-shapes-api-server.md), and use it as an inspiration or starting point for your own collection. - -The Approov tokens used in the headers of this Postman collection where manually created with bash commands that used the dummy secret set on the `.env.example` file to sign all the Approov tokens. - -If you are using the Aproov secret retrieved with the [Approov CLI]((https://approov.io/docs/latest/approov-cli-tool-reference/)) tool then you need to use it to generate some valid and invalid tokens. Some examples of using it can be found in the Approov [docs](https://approov.io/docs/latest/approov-usage-documentation/#generating-example-tokens). - - -## INSTALL DEPENDENCIES - -If not already using the NPM packages `express-jwt` and `dotenv` in your -project, please add them: - -```bash -npm install --save express-jwt dotenv debug -``` - -## ORIGINAL SERVER - -Let's use the [original-server.js](./original-server.js) as an example -for a current server where we want to add Approov to protect some or all the -endpoints. - -After we add only the necessary code to integrate Approov, the end result -will look like we have now in the [approov-protected-server.js](./approov-protected-server.js). - - -## HOW TO INTEGRATE - -We will learn how to go from the [original-server.js](./original-server.js) to the -[approov-protected-server.js](./approov-protected-server.js) and how to configure the server. - -In order to be able to check the Approov token the `express-jwt` library needs -to know the secret used by the Approov cloud service to sign it. A secure way to -do this is by passing it as an environment variable, as you can see we have done -[here](./configuration.js#L75). - -Next we need to define two callbacks to be used during the Approov token check -process. One callback is to perform the check itself with the library -`express-jwt` and the other is to handle any errors occurred during that check. - -Let's breakdown the example implementation to make it easier to adapt to your -current project. - - -### Require Dependencies - -We need to require the dependencies we installed before. - -[Configuration file](./configuration.js#L1): - -```js -// file: configuration.js - -// if not already in use add: -require('dotenv').config() -``` - -[Approov Protected Server file](./approov-protected-server.js#L2-L3): - -```js -// file: approov-protected-server.js - -const { expressjwt: jwt } = require('express-jwt') -const crypto = require('crypto') -``` - -### Setup Environment - -If you don't have already an `.env` file, then you need to create one in the -root of your project by using this [.env.example](./.env.example) as your -starting point. - -The `.env` file must contain the following variables: - -```env -# Feel free to play with different secrets. For development only you can create them with: -# $ openssl rand -base64 64 | tr -d '\n'; echo -APPROOV_BASE64_SECRET=h+CX0tOzdAAR9l15bWAqvq7w9olk66daIH+Xk+IAHhVVHszjDzeGobzNnqyRze3lw/WVyWrc2gZfh3XXfBOmww== -APPROOV_ABORT_REQUEST_ON_INVALID_TOKEN=true -APPROOV_ABORT_REQUEST_ON_INVALID_TOKEN_BINDING=true -APPROOV_LOGGING_ENABLED=true -``` - - -Now we can read them from our code, like is done in the [configuration file](./configuration.js#L50-L86): - -```js -// file: configuration.js - -/////////////////////////// -/// APPROOV ENVIRONMENT -////////////////////////// - -let isToAbortRequestOnInvalidToken = true -let isToAbortOnInvalidClaim = true -let isApproovLoggingEnabled = true -const abortRequestOnInvalidToken = dotenv.parsed.APPROOV_ABORT_REQUEST_ON_INVALID_TOKEN || 'true' -const abortOnInvalidClaim = dotenv.parsed.APPROOV_ABORT_REQUEST_ON_INVALID_TOKEN_BINDING || 'true' -const approovLoggingEnabled = dotenv.parsed.APPROOV_LOGGING_ENABLED || 'true' - -if (abortRequestOnInvalidToken.toLowerCase() === 'false') { - isToAbortRequestOnInvalidToken = false -} - -if (abortOnInvalidClaim.toLowerCase() === 'false') { - isToAbortOnInvalidClaim = false -} - -if (approovLoggingEnabled.toLowerCase() === 'false') { - isApproovLoggingEnabled = false -} - -const approov = { - abortRequestOnInvalidToken: isToAbortRequestOnInvalidToken, - abortRequestOnInvalidTokenBinding: isToAbortOnInvalidClaim, - approovLoggingEnabled: isApproovLoggingEnabled, - - // The Approov base64 secret must be retrieved with the Approov CLI tool - base64Secret: dotenv.parsed.APPROOV_BASE64_SECRET, -} - - -//////////////////////////// -/// EXPORT CONFIGURATION -/////////////////////////// - -module.exports = { - server, - approov, -} -``` - -### Customizable Approov Callbacks - -This are callbacks used in the Approov Integration that you may want to -customize to the needs of your application. - -Lets's start with the logging callback that we can see in [approov-protected-server.js](./approov-protected-server.js#L72-L75): - -```js -// file: approov-protected-server.js - -// Callback to be customized with your preferred way of logging. -const logApproov = function(req, res, message) { - debug(buildLogMessagePrefix(req, res) + ' ' + message) -} -``` - -Next we have the callback to get the token binding header for the Approov token, as seen in [approov-protected-server.js](./approov-protected-server.js#L77-L83): - -```js -// file: approov-protected-server.js - -// Callback to be personalized in order to get the token binding header value being used by -// your application. -// In the current scenario we use an Authorization token, but feel free to use what -// suits best your needs. -const getTokenBindingHeader = function(req) { - return req.get('Authorization') -} -``` - -Each time an Approov token doesn't validate for any reason the Approov -integration will pass the control to the application logic by invoking a callback as the one defined in [approov-protected-server.js](./approov-protected-server.js#L85-L106): - -```js -// file: approov-protected-server.js - -// Callback to be customized with how you want to handle a request with an -// invalid Approov token. -// The code included in this callback is provided as an example, that you can -// keep or totally change it in a way that best suits your needs. -const handlesRequestWithInvalidApproovToken = function(err, req, res, next, httpStatusCode) { - - logApproov(req, res, 'APPROOV TOKEN: ' + err) - - // Logging a message to make clear in the logs what was the action we took. - // Feel free to skip it if you think is not necessary to your use case. - let message = 'REQUEST WITH INVALID APPROOV TOKEN' - - if (config.approov.abortRequestOnInvalidToken === true) { - buildBadRequestResponse(req, res, httpStatusCode, 'REJECTED ' + message) - return - } - - message = 'ACCEPTED ' + message - logApproov(req, res, message) - next() - return -} -``` - -Now we have another callback that will be invoked by the Approov integration to -allow the application to decide what action to take when it fails to validate the token -binding in the Approov token, as defined in [approov-protected-server.js](./approov-protected-server.js#L108-L128): - -```js -// file: approov-protected-server.js - -// Callback to be customized with how you want to handle a request where the -// token binding in the request header doesn't match the the one in the Approov token. -// The code included in this callback is provided as an example, that you can -// keep or totally change it in a way that best suits your needs. -const handlesRequestWithInvalidTokenBinding = function(req, res, next, httpStatusCode, message) { - - logApproov(req, res, message) - - // Logging here to make clear in the logs what was the action we took. - // Feel free to skip it if you think is not necessary to your use case. - let logMessage = 'REQUEST WITH INVALID APPROOV TOKEN BINDING' - - if (config.approov.abortRequestOnInvalidTokenBinding === true) { - buildBadRequestResponse(req, res, httpStatusCode, 'REJECTED ' + logMessage) - return - } - - logApproov(req, res, 'ACCEPTED ' + logMessage) - next() - return -} -``` - -The last callback is the one where you can customize the bad request response, as defined in [approov-protected-server.js](./approov-protected-server.js#L130-L135): - -```js -// file: approov-protected-server.js - -// Callback to build the response when a request fails to pass the Approov checks. -const buildBadRequestResponse = function(req, res, httpStatusCode, logMessage) { - res.status(httpStatusCode) - logApproov(req, res, logMessage) - res.json({}) -} -``` - -### Approov Integration Core Callbacks - -This core callbacks are specific to the Approov integration and once they don't -interfere with the flow or behavior of your application we think they are not -in need of being customized. - -#### Helper Functions - -The core callbacks will need some very basic helper functions, like the ones -defined in the [approov-protected-server](./approov-protected-server.js#L149-L159): - -```js -// file: approov-protected-server.js - -const isEmpty = function(value) { - return (value === undefined) || (value === null) || (value === '') -} - -const isString = function(value) { - return (typeof(value) === 'string') -} - -const isEmptyString = function(value) { - return (isEmpty(value) === true) || (isString(value) === false) || (value.trim() === '') -} -``` - -#### Approov Token - -[This callback](./approov-protected-server.js#L165-L181) will be used in -the middleware to check the Approov token: - -```js -// file: approov-protected-server.js - -// Callback that performs the Approov token check using the express-jwt library -const checkApproovToken = jwt({ - secret: Buffer.from(config.approov.base64Secret, 'base64'), // decodes the Approov secret - requestProperty: 'approovTokenDecoded', - getToken: function fromApproovTokenHeader(req, res) { - req.approovTokenError = false - const approovToken = req.get('Approov-Token') - - if (isEmptyString(approovToken)) { - req.approovTokenError = true - throw new Error('token empty or missing in the header of the request.') - } - - return approovToken - }, - algorithms: ['HS256'] -}) -``` - -Then we need [this callback](./approov-protected-server.js#L183-L204) to -handle any error that may occur during the Approov token check: - -```js -// file: approov-protected-server.js - -// Callback to handle the errors occurred while checking the Approov token. -const handlesApproovTokenError = function(err, req, res, next) { - - if (req.approovTokenError === true) { - // When we reach here, it means the header `Approov-Token` is empty or is missing. - // @see checkApproovToken() - handlesRequestWithInvalidApproovToken(err, req, res, next, 400) - return - } - - if (err.name === 'UnauthorizedError') { - // When we reach here, it means that an Error was thrown by the express-jwt - // library while decoding the Approov token. - // @see checkApproovToken() - req.approovTokenError = true - handlesRequestWithInvalidApproovToken(err, req, res, next, 401) - return - } - - next() - return -} -``` - -Finally we want to handle when the Approov token succeeds the validation process, -as we have done [approov-protected-server.js](./approov-protected-server.js#L206-L215) - -```js -// file: approov-protected-server.js - -// Callback to handles when an Approov token is successfully validated. -const handlesApproovTokenSuccess = function(req, res, next) { - - if (req.approovTokenError === false) { - logApproov(req, res, 'ACCEPTED REQUEST WITH VALID APPROOV TOKEN') - } - - next() - return -} -``` - -#### Approov Token Binding - -We will use this two functions to validate if the token binding header in the request -matches the one in the Approov token, as we can see in the [approov-protected-server.js](./approov-protected-server.js#L221-L267): - -```js -// file: approov-protected-server.js - -// Callback to check the Approov token binding in the header matches with the one in the key `pay` of the Approov token claims. -const handlesApproovTokenBindingVerification = function(req, res, next){ - - if (req.approovTokenError === true) { - next() - return - } - - // The decoded Approov token was added to the request object when the checked it at `checkApproovToken()` - token_binding_payload = req.approovTokenDecoded.pay - - if (token_binding_payload === undefined) { - handlesRequestWithInvalidTokenBinding(req, res, next, 400, "APPROOV TOKEN BINDING ERROR: key 'pay' is missing in the claims of the Approov token payload.") - return - } - - if (isEmptyString(token_binding_payload)) { - handlesRequestWithInvalidTokenBinding(req, res, next, 400, "APPROOV TOKEN BINDING ERROR: key 'pay' in the decoded token is empty.") - return - } - - // We use here the Authorization token, but feel free to use another header, but you need to bind this header to - // the Approov token in the mobile app. - const token_binding_header = getTokenBindingHeader(req) - - if (isEmptyString(token_binding_header)) { - handlesRequestWithInvalidTokenBinding(req, res, next, 400, "APPROOV TOKEN BINDING ERROR: Missing or empty header to perform the verification for the token binding.") - return - } - - // We need to hash and base64 encode the token binding header, because that's how it was included in the Approov - // token on the mobile app. - const token_binding_header_encoded = crypto.createHash('sha256').update(token_binding_header, 'utf-8').digest('base64') - - if (token_binding_payload !== token_binding_header_encoded) { - handlesRequestWithInvalidTokenBinding(req, res, next, 401, "APPROOV TOKEN BINDING ERROR: token binding in header doesn't match with the key 'pay' in the decoded token.") - return - } - - logApproov(req, res, 'ACCEPTED REQUEST WITH VALID APPROOV TOKEN BINDING') - - // Let the request continue as usual. - next() - return -} -``` - - -### Middleware - -We will use the middleware approach to intercept all endpoints we want to protect -with an Approov Token. So any interceptor must be placed before we declare the -endpoints we want to protect, like is done in the -[approov-protected-server.js](./approov-protected-server.js#L271-L292). - -The following examples will use the callbacks we already have defined -[here](#approov-integration-core-callbacks) to pass as the second parameter to -the middleware interceptors. - -#### For specific endpoints - -To protect specific endpoints in a current server we only need to add the Approov -interceptors for each endpoint we want to protect, as we have done [here](./approov-protected-server.js#L271-L292): - -```js -// file: approov-protected-server.js - -// Intercepts all calls to the shapes endpoint to validate the Approov token. -app.use('/v2/shapes', checkApproovToken) - -// Handles failure in validating the Approov token -app.use('/v2/shapes', handlesApproovTokenError) - -// Handles requests where the Approov token is a valid one. -app.use('/v2/shapes', handlesApproovTokenSuccess) - -// Intercepts all calls to the forms endpoint to validate the Approov token. -app.use('/v2/forms', checkApproovToken) - -// Handles failure in validating the Approov token -app.use('/v2/forms', handlesApproovTokenError) - -// Handles requests where the Approov token is a valid one. -app.use('/v2/forms', handlesApproovTokenSuccess) - -// Checks if the Approov token binding is valid and aborts the request when the environment variable -// APPROOV_ABORT_REQUEST_ON_INVALID_TOKEN_BINDING is set to true in the environment file. -app.use('/v2/forms', handlesApproovTokenBindingVerification) -``` - -#### For all endpoints - -To protect all endpoints under a certain path, we only need to declare the Approov interceptor for the root of that path, for example if we want to protect all endpoints under `/v2` we need to modify the above code to look like this: - -```js -// file: approov-protected-server.js - -// Intercepts all calls to the shapes endpoint to validate the Approov token. -app.use('/v2', checkApproovToken) - -// Handles failure in validating the Approov token -app.use('/v2', handlesApproovTokenError) - -// Handles requests where the Approov token is a valid one. -app.use('/v2', handlesApproovTokenSuccess) - -// only use in the root endpoint `/v2` if you want to validate the Approov token binding -// in all endpoints, otherwise declare it per endpoint you want to protect. -app.use('/v2', handlesApproovTokenBindingVerification) -``` - -To protect every single endpoint in your API, you would replace `/v2/` in above example with only `/`. - - -### The Code Difference - -If we compare the [original-server.js](./original-server.js) with the -[approov-protected-server.js](./approov-protected-server.js) we will see -this file difference: - -```js ---- /home/sublime/workspace/node/express/server/original-server.js -+++ /home/sublime/workspace/node/express/server/approov-protected-server.js -@@ -1,4 +1,6 @@ --const debug = require('debug')('original-server') -+const debug = require('debug')('approov-protected-server') -+const jwt = require('express-jwt') -+const crypto = require('crypto') - const config = require('./configuration') - const https = require('https') - const fs = require('fs') -@@ -60,6 +62,243 @@ - } - - -+//////////////////////////////////////////////////////////////////////////////// -+/// YOUR APPLICATION CUSTOMIZABLE CALLBACKS FOR THE APPROOV INTEGRATION -+//////////////////////////////////////////////////////////////////////////////// -+/// -+/// Feel free to customize this callbacks to best suite the needs your needs. -+/// -+ -+// Callback to be customized with your preferred way of logging. -+const logApproov = function(req, res, message) { -+ debug(buildLogMessagePrefix(req, res) + ' ' + message) -+} -+ -+// Callback to be personalized in order to get the token binding header value being used by -+// your application. -+// In the current scenario we use an Authorization token, but feel free to use what -+// suits best your needs. -+const getTokenBindingHeader = function(req) { -+ return req.get('Authorization') -+} -+ -+// Callback to be customized with how you want to handle a request with an -+// invalid Approov token. -+// The code included in this callback is provided as an example, that you can -+// keep or totally change it in a way that best suits your needs. -+const handlesRequestWithInvalidApproovToken = function(err, req, res, next, httpStatusCode) { -+ -+ logApproov(req, res, 'APPROOV TOKEN: ' + err) -+ -+ // Logging a message to make clear in the logs what was the action we took. -+ // Feel free to skip it if you think is not necessary to your use case. -+ let message = 'REQUEST WITH INVALID APPROOV TOKEN' -+ -+ if (config.approov.abortRequestOnInvalidToken === true) { -+ buildBadRequestResponse(req, res, httpStatusCode, 'REJECTED ' + message) -+ return -+ } -+ -+ message = 'ACCEPTED ' + message -+ logApproov(req, res, message) -+ next() -+ return -+} -+ -+// Callback to be customized with how you want to handle a request where the -+// token binding in the request header doesn't match the the one in the Approov token. -+// The code included in this callback is provided as an example, that you can -+// keep or totally change it in a way that best suits your needs. -+const handlesRequestWithInvalidTokenBinding = function(req, res, next, httpStatusCode, message) { -+ -+ logApproov(req, res, message) -+ -+ // Logging here to make clear in the logs what was the action we took. -+ // Feel free to skip it if you think is not necessary to your use case. -+ let logMessage = 'REQUEST WITH INVALID APPROOV TOKEN BINDING' -+ -+ if (config.approov.abortRequestOnInvalidTokenBinding === true) { -+ buildBadRequestResponse(req, res, httpStatusCode, 'REJECTED ' + logMessage) -+ return -+ } -+ -+ logApproov(req, res, 'ACCEPTED ' + logMessage) -+ next() -+ return -+} -+ -+// Callback to build the response when a request fails to pass the Approov checks. -+const buildBadRequestResponse = function(req, res, httpStatusCode, logMessage) { -+ res.status(httpStatusCode) -+ logApproov(req, res, logMessage) -+ res.json({}) -+} -+ -+ -+//////////////////////////////////////////////////////////////////////////////// -+/// STARTS NON CUSTOMIZABLE LOGIC FOR THE APPROOV INTEGRATION -+//////////////////////////////////////////////////////////////////////////////// -+/// -+/// This section contains code that is specific to the Approov integration, -+/// thus we think that is not necessary to customize it, once is not -+/// interfering with your application logic or behavior. -+/// -+ -+////// APPROOV HELPER FUNCTIONS ////// -+ -+const isEmpty = function(value) { -+ return (value === undefined) || (value === null) || (value === '') -+} -+ -+const isString = function(value) { -+ return (typeof(value) === 'string') -+} -+ -+const isEmptyString = function(value) { -+ return (isEmpty(value) === true) || (isString(value) === false) || (value.trim() === '') -+} -+ -+ -+////// APPROOV TOKEN ////// -+ -+ -+// Callback that performs the Approov token check using the express-jwt library -+const checkApproovToken = jwt({ -+ secret: Buffer.from(config.approov.base64Secret, 'base64'), // decodes the Approov secret -+ requestProperty: 'approovTokenDecoded', -+ getToken: function fromApproovTokenHeader(req, res) { -+ req.approovTokenError = false -+ const approovToken = req.get('Approov-Token') -+ -+ if (isEmptyString(approovToken)) { -+ req.approovTokenError = true -+ throw new Error('token empty or missing in the header of the request.') -+ } -+ -+ return approovToken -+ }, -+ algorithms: ['HS256'] -+}) -+ -+// Callback to handle the errors occurred while checking the Approov token. -+const handlesApproovTokenError = function(err, req, res, next) { -+ -+ if (req.approovTokenError === true) { -+ // When we reach here, it means the header `Approov-Token` is empty or is missing. -+ // @see checkApproovToken() -+ handlesRequestWithInvalidApproovToken(err, req, res, next, 400) -+ return -+ } -+ -+ if (err.name === 'UnauthorizedError') { -+ // When we reach here, it means that an Error was thrown by the express-jwt -+ // library while decoding the Approov token. -+ // @see checkApproovToken() -+ req.approovTokenError = true -+ handlesRequestWithInvalidApproovToken(err, req, res, next, 401) -+ return -+ } -+ -+ next() -+ return -+} -+ -+// Callback to handles when an Approov token is successfully validated. -+const handlesApproovTokenSuccess = function(req, res, next) { -+ -+ if (req.approovTokenError === false) { -+ logApproov(req, res, 'ACCEPTED REQUEST WITH VALID APPROOV TOKEN') -+ } -+ -+ next() -+ return -+} -+ -+ -+////// APPROOV TOKEN BINDING ////// -+ -+ -+// Callback to check the Approov token binding in the header matches with the one in the key `pay` of the Approov token claims. -+const handlesApproovTokenBindingVerification = function(req, res, next){ -+ -+ if (req.approovTokenError === true) { -+ next() -+ return -+ } -+ -+ // The decoded Approov token was added to the request object when the checked it at `checkApproovToken()` -+ token_binding_payload = req.approovTokenDecoded.pay -+ -+ if (token_binding_payload === undefined) { -+ handlesRequestWithInvalidTokenBinding(req, res, next, 400, "APPROOV TOKEN BINDING ERROR: key 'pay' is missing in the claims of the Approov token payload.") -+ return -+ } -+ -+ if (isEmptyString(token_binding_payload)) { -+ handlesRequestWithInvalidTokenBinding(req, res, next, 400, "APPROOV TOKEN BINDING ERROR: key 'pay' in the decoded token is empty.") -+ return -+ } -+ -+ // We use here the Authorization token, but feel free to use another header, but you need to bind this header to -+ // the Approov token in the mobile app. -+ const token_binding_header = getTokenBindingHeader(req) -+ -+ if (isEmptyString(token_binding_header)) { -+ handlesRequestWithInvalidTokenBinding(req, res, next, 400, "APPROOV TOKEN BINDING ERROR: Missing or empty header to perform the verification for the token binding.") -+ return -+ } -+ -+ // We need to hash and base64 encode the token binding header, because that's how it was included in the Approov -+ // token on the mobile app. -+ const token_binding_header_encoded = crypto.createHash('sha256').update(token_binding_header, 'utf-8').digest('base64') -+ -+ if (token_binding_payload !== token_binding_header_encoded) { -+ handlesRequestWithInvalidTokenBinding(req, res, next, 401, "APPROOV TOKEN BINDING ERROR: token binding in header doesn't match with the key 'pay' in the decoded token.") -+ return -+ } -+ -+ logApproov(req, res, 'ACCEPTED REQUEST WITH VALID APPROOV TOKEN BINDING') -+ -+ // Let the request continue as usual. -+ next() -+ return -+} -+ -+/////// THE APPROOV INTERCEPTORS /////// -+ -+// Intercepts all calls to the shapes endpoint to validate the Approov token. -+app.use('/v2/shapes', checkApproovToken) -+ -+// Handles failure in validating the Approov token -+app.use('/v2/shapes', handlesApproovTokenError) -+ -+// Handles requests where the Approov token is a valid one. -+app.use('/v2/shapes', handlesApproovTokenSuccess) -+ -+// Intercepts all calls to the forms endpoint to validate the Approov token. -+app.use('/v2/forms', checkApproovToken) -+ -+// Handles failure in validating the Approov token -+app.use('/v2/forms', handlesApproovTokenError) -+ -+// Handles requests where the Approov token is a valid one. -+app.use('/v2/forms', handlesApproovTokenSuccess) -+ -+// Checks if the Approov token binding is valid and aborts the request when the environment variable -+// APPROOV_ABORT_REQUEST_ON_INVALID_TOKEN_BINDING is set to true in the environment file. -+app.use('/v2/forms', handlesApproovTokenBindingVerification) -+ -+/// NOTE: -+/// Is important to place all the Approov interceptors before we declare the -+/// endpoints of the API, otherwise they will not be able to intercept any -+/// request. -+ -+//////////////////////////////////////////////////////////////////////////////// -+/// ENDS APPOOV INTEGRATION -+//////////////////////////////////////////////////////////////////////////////// -+ - //////////////// - // ENDPOINTS - //////////////// -@@ -84,6 +323,28 @@ - app.get('/v1/forms', function(req, res, next) { - logResponseToRequest(req, res) - buildFormsResponse(res, 'unprotected') -+}) -+ -+/** -+ * V2 ENDPOINTS -+ */ -+ -+// simple 'hello world' endpoint. -+app.get('/v2/hello', function (req, res, next) { -+ logResponseToRequest(req, res) -+ buildHelloWorldResponse(res) -+}) -+ -+// shapes endpoint returns a random shape. -+app.get('/v2/shapes', function(req, res, next) { -+ logResponseToRequest(req, res) -+ buildShapesResponse(res, 'protected') -+}) -+ -+// shapes endpoint returns a random form. -+app.get('/v2/forms', function(req, res, next) { -+ logResponseToRequest(req, res) -+ buildFormsResponse(res, 'protected') - }) -``` - -As we can see the Approov integration in a current server is simple, easy and is -done with just a few lines of code. - -If you have not done it already, now is time to follow the -[Approov Shapes API Server](./docs/approov-shapes-api-server.md) walk-through -to see and have a feel for how all this works. - - -## PRODUCTION - -In order to protect the communication between your mobile app and the API server -is important to only communicate hover a secure communication channel, aka HTTPS, and to use certificate pinning. - -We do not use HTTPS and certificate pinning in this Approov integration example -because we want to be able to run the [Approov Shapes API Server](./docs/approov-shapes-api-server.md) in localhost. - -Please bear in mind that HTTPS on its own is not enough, certificate pinning -must be also used to pin the connection between the mobile app and the API -server in order to prevent Man in the Middle Attacks and Approov provides out of the box [Dynamic Certificate Pinning](https://approov.io/product/dynamic-cert-pinning) to allow your mobile app to pin the connection to your API server without for you to have to write a single line of code, while giving you the ability to update the pins remotely with the [Approov CLI Tool](https://approov.io/docs/latest/approov-cli-tool-reference/#api-command). Yes you will not need to release a new mobile app to revoke/rotate certificates. diff --git a/servers/shapes-api/approov-protected-server.js b/servers/shapes-api/approov-protected-server.js deleted file mode 100644 index 984179e..0000000 --- a/servers/shapes-api/approov-protected-server.js +++ /dev/null @@ -1,373 +0,0 @@ -const debug = require('debug')('approov-protected-server') -const { expressjwt: jwt } = require('express-jwt') -const crypto = require('crypto') -const config = require('./configuration') -const https = require('https') -const fs = require('fs') -const express = require('express') -const cors = require('cors') -const path = require('path') -const app = express() -app.use(cors()) - - -//////////////// -/// FUNCTIONS -//////////////// - -const buildLogMessagePrefix = function(req, res) { - return res.statusCode + ' ' + req.method + ' ' + req.originalUrl -} - -const logResponseToRequest = function(req, res) { - debug(buildLogMessagePrefix(req, res)) -} - -const getRandomShapeResponse = function() { - const shapes = [ - 'Circle', - 'Triangle', - 'Square', - 'Rectangle' - ] - return { - shape: shapes[Math.floor((Math.random() * shapes.length))] - } -} - -const getRandomFormResponse = function() { - const forms = [ - 'Sphere', - 'Cone', - 'Cube', - 'Box' - ] - return {"form": forms[Math.floor((Math.random() * forms.length))]} -} - -const buildHelloWorldResponse = function(res) { - res.json({ - text: "Hello, World!", - }) -} - -const buildShapesResponse = function(res, protectionStatus) { - const response = getRandomShapeResponse() - res.json(response) -} - -const buildFormsResponse = function(res, protectionStatus) { - const response = getRandomFormResponse() - res.json(response) -} - - -//////////////////////////////////////////////////////////////////////////////// -/// YOUR APPLICATION CUSTOMIZABLE CALLBACKS FOR THE APPROOV INTEGRATION -//////////////////////////////////////////////////////////////////////////////// -/// -/// Feel free to customize this callbacks to best suite the needs your needs. -/// - -// Callback to be customized with your preferred way of logging. -const logApproov = function(req, res, message) { - debug(buildLogMessagePrefix(req, res) + ' ' + message) -} - -// Callback to be personalized in order to get the token binding header value being used by -// your application. -// In the current scenario we use an Authorization token, but feel free to use what -// suits best your needs. -const getTokenBindingHeader = function(req) { - return req.get('Authorization') -} - -// Callback to be customized with how you want to handle a request with an -// invalid Approov token. -// The code included in this callback is provided as an example, that you can -// keep or totally change it in a way that best suits your needs. -const handlesRequestWithInvalidApproovToken = function(err, req, res, next, httpStatusCode) { - - logApproov(req, res, 'APPROOV TOKEN: ' + err) - - // Logging a message to make clear in the logs what was the action we took. - // Feel free to skip it if you think is not necessary to your use case. - let message = 'REQUEST WITH INVALID APPROOV TOKEN' - - if (config.approov.abortRequestOnInvalidToken === true) { - buildBadRequestResponse(req, res, httpStatusCode, 'REJECTED ' + message) - return - } - - message = 'ACCEPTED ' + message - logApproov(req, res, message) - next() - return -} - -// Callback to be customized with how you want to handle a request where the -// token binding in the request header doesn't match the the one in the Approov token. -// The code included in this callback is provided as an example, that you can -// keep or totally change it in a way that best suits your needs. -const handlesRequestWithInvalidTokenBinding = function(req, res, next, httpStatusCode, message) { - - logApproov(req, res, message) - - // Logging here to make clear in the logs what was the action we took. - // Feel free to skip it if you think is not necessary to your use case. - let logMessage = 'REQUEST WITH INVALID APPROOV TOKEN BINDING' - - if (config.approov.abortRequestOnInvalidTokenBinding === true) { - buildBadRequestResponse(req, res, httpStatusCode, 'REJECTED ' + logMessage) - return - } - - logApproov(req, res, 'ACCEPTED ' + logMessage) - next() - return -} - -// Callback to build the response when a request fails to pass the Approov checks. -const buildBadRequestResponse = function(req, res, httpStatusCode, logMessage) { - res.status(httpStatusCode) - logApproov(req, res, logMessage) - res.json({}) -} - - -//////////////////////////////////////////////////////////////////////////////// -/// STARTS NON CUSTOMIZABLE LOGIC FOR THE APPROOV INTEGRATION -//////////////////////////////////////////////////////////////////////////////// -/// -/// This section contains code that is specific to the Approov integration, -/// thus we think that is not necessary to customize it, once is not -/// interfering with your application logic or behavior. -/// - -////// APPROOV HELPER FUNCTIONS ////// - -const isEmpty = function(value) { - return (value === undefined) || (value === null) || (value === '') -} - -const isString = function(value) { - return (typeof(value) === 'string') -} - -const isEmptyString = function(value) { - return (isEmpty(value) === true) || (isString(value) === false) || (value.trim() === '') -} - - -////// APPROOV TOKEN ////// - - -// Callback that performs the Approov token check using the express-jwt library -const checkApproovToken = jwt({ - secret: Buffer.from(config.approov.base64Secret, 'base64'), // decodes the Approov secret - requestProperty: 'approovTokenDecoded', - getToken: function fromApproovTokenHeader(req, res) { - req.approovTokenError = false - const approovToken = req.get('Approov-Token') - - if (isEmptyString(approovToken)) { - req.approovTokenError = true - throw new Error('token empty or missing in the header of the request.') - } - - return approovToken - }, - algorithms: ['HS256'] -}) - -// Callback to handle the errors occurred while checking the Approov token. -const handlesApproovTokenError = function(err, req, res, next) { - - if (req.approovTokenError === true) { - // When we reach here, it means the header `Approov-Token` is empty or is missing. - // @see checkApproovToken() - handlesRequestWithInvalidApproovToken(err, req, res, next, 400) - return - } - - if (err.name === 'UnauthorizedError') { - // When we reach here, it means that an Error was thrown by the express-jwt - // library while decoding the Approov token. - // @see checkApproovToken() - req.approovTokenError = true - handlesRequestWithInvalidApproovToken(err, req, res, next, 401) - return - } - - next() - return -} - -// Callback to handles when an Approov token is successfully validated. -const handlesApproovTokenSuccess = function(req, res, next) { - - if (req.approovTokenError === false) { - logApproov(req, res, 'ACCEPTED REQUEST WITH VALID APPROOV TOKEN') - } - - next() - return -} - - -////// APPROOV TOKEN BINDING ////// - - -// Callback to check the Approov token binding in the header matches with the one in the key `pay` of the Approov token claims. -const handlesApproovTokenBindingVerification = function(req, res, next){ - - if (req.approovTokenError === true) { - next() - return - } - - // The decoded Approov token was added to the request object when the checked it at `checkApproovToken()` - token_binding_payload = req.approovTokenDecoded.pay - - if (token_binding_payload === undefined) { - handlesRequestWithInvalidTokenBinding(req, res, next, 400, "APPROOV TOKEN BINDING ERROR: key 'pay' is missing in the claims of the Approov token payload.") - return - } - - if (isEmptyString(token_binding_payload)) { - handlesRequestWithInvalidTokenBinding(req, res, next, 400, "APPROOV TOKEN BINDING ERROR: key 'pay' in the decoded token is empty.") - return - } - - // We use here the Authorization token, but feel free to use another header, but you need to bind this header to - // the Approov token in the mobile app. - const token_binding_header = getTokenBindingHeader(req) - - if (isEmptyString(token_binding_header)) { - handlesRequestWithInvalidTokenBinding(req, res, next, 400, "APPROOV TOKEN BINDING ERROR: Missing or empty header to perform the verification for the token binding.") - return - } - - // We need to hash and base64 encode the token binding header, because that's how it was included in the Approov - // token on the mobile app. - const token_binding_header_encoded = crypto.createHash('sha256').update(token_binding_header, 'utf-8').digest('base64') - - if (token_binding_payload !== token_binding_header_encoded) { - handlesRequestWithInvalidTokenBinding(req, res, next, 401, "APPROOV TOKEN BINDING ERROR: token binding in header doesn't match with the key 'pay' in the decoded token.") - return - } - - logApproov(req, res, 'ACCEPTED REQUEST WITH VALID APPROOV TOKEN BINDING') - - // Let the request continue as usual. - next() - return -} - -/////// THE APPROOV INTERCEPTORS /////// - -// Intercepts all calls to the shapes endpoint to validate the Approov token. -app.use('/v2/shapes', checkApproovToken) - -// Handles failure in validating the Approov token -app.use('/v2/shapes', handlesApproovTokenError) - -// Handles requests where the Approov token is a valid one. -app.use('/v2/shapes', handlesApproovTokenSuccess) - -// Intercepts all calls to the forms endpoint to validate the Approov token. -app.use('/v2/forms', checkApproovToken) - -// Handles failure in validating the Approov token -app.use('/v2/forms', handlesApproovTokenError) - -// Handles requests where the Approov token is a valid one. -app.use('/v2/forms', handlesApproovTokenSuccess) - -// Checks if the Approov token binding is valid and aborts the request when the environment variable -// APPROOV_ABORT_REQUEST_ON_INVALID_TOKEN_BINDING is set to true in the environment file. -app.use('/v2/forms', handlesApproovTokenBindingVerification) - -/// NOTE: -/// Is important to place all the Approov interceptors before we declare the -/// endpoints of the API, otherwise they will not be able to intercept any -/// request. - -//////////////////////////////////////////////////////////////////////////////// -/// ENDS APPOOV INTEGRATION -//////////////////////////////////////////////////////////////////////////////// - -//////////////// -// ENDPOINTS -//////////////// - -/** - * V1 ENDPOINTS - */ - -// simple 'hello world' endpoint. -app.get('/v1/hello', function (req, res, next) { - logResponseToRequest(req, res) - buildHelloWorldResponse(res) -}) - -// shapes endpoint returns a random shape. -app.get('/v1/shapes', function(req, res, next) { - logResponseToRequest(req, res) - buildShapesResponse(res, 'unprotected') -}) - -// shapes endpoint returns a random form. -app.get('/v1/forms', function(req, res, next) { - logResponseToRequest(req, res) - buildFormsResponse(res, 'unprotected') -}) - -/** - * V2 ENDPOINTS - */ - -// simple 'hello world' endpoint. -app.get('/v2/hello', function (req, res, next) { - logResponseToRequest(req, res) - buildHelloWorldResponse(res) -}) - -// shapes endpoint returns a random shape. -app.get('/v2/shapes', function(req, res, next) { - logResponseToRequest(req, res) - buildShapesResponse(res, 'protected') -}) - -// shapes endpoint returns a random form. -app.get('/v2/forms', function(req, res, next) { - logResponseToRequest(req, res) - buildFormsResponse(res, 'protected') -}) - - -//////////// -// SERVER -//////////// - -if (config.server.httpsEnabled) { - // Load the certificate and key data for our server to be hosted over HTTPS - const serverOptions = { - key: fs.readFileSync(config.server.certificateKey), - cert: fs.readFileSync(config.server.certificatePem), - requestCert: false, - rejectUnauthorized: false - } - - // Create and run the HTTPS server - https.createServer(serverOptions, app).listen(config.server.httpPort, function() { - debug("Shapes server listening on %s", config.server.fullUrl) - }) - -} else { - - // Create and run the HTTP server - app.listen(config.server.httpPort, function () { - debug("Shapes server listening on %s", config.server.fullUrl) - }) -} diff --git a/servers/shapes-api/bin/stack.bash b/servers/shapes-api/bin/stack.bash deleted file mode 100755 index 3bd1366..0000000 --- a/servers/shapes-api/bin/stack.bash +++ /dev/null @@ -1,89 +0,0 @@ -#!/bin/bash - -set -eu - -CONTAINER_USER="$(id -u)" - -HTTP_PORT=8002 - -function Show_Help -{ - echo && cat ./docker/usage-help.txt && echo -} - -function Build_Docker_Image() -{ - sudo docker build -t approov/nodejs-express ./docker -} - -function Create_Docker_Container -{ - local _command="${1:-zsh}" - local _user="${2? Missing user name or uid for the container we want to stop!!!}" - local _port="${3? Missing http port for the container we want to stop!!!}" - local _server_name="${4? Missing server name for the container we want to stop!!!}" - local _container_name="nodejs-express-${_server_name}" - - sudo docker run \ - -it \ - --rm \ - --user "${_user}" \ - --env-file .env \ - --env "SERVER_NAME=./${_server_name}.js" \ - --env "HTTP_PORT=${_port}" \ - --name "${_container_name}" \ - --publish "127.0.0.1:${_port}:${_port}" \ - --workdir "/home/node/workspace" \ - --volume "$PWD:/home/node/workspace" \ - approov/nodejs-express ${_command} -} - -function Stop_Docker_Container -{ - local _user="${1? Missing user name or uid for the container we want to stop!!!}" - local _port="${2? Missing http port for the container we want to stop!!!}" - local _server_name="${3? Missing server name for the container we want to stop!!!}" - local _container_name="nodejs-express-${_server_name}" - - sudo docker container stop "${_container_name}" -} - -for input in "${@}" - do - case "${input}" in - build) - Build_Docker_Image - exit 0 - ;; - -p | --port) - HTTP_PORT="${2? Missing HTTP port to access the container in localhost}" - shift 2 - ;; - -u | --user) - CONTAINER_USER="${2? Missing user name or uid to use inside the container}" - shift 2 - ;; - approov-protected-server) - Create_Docker_Container "npm start" "${CONTAINER_USER}" "${HTTP_PORT}" "approov-protected-server" - exit 0 - ;; - original-server) - Create_Docker_Container "npm run original-server" "${CONTAINER_USER}" "${HTTP_PORT}" "original-server" - exit 0 - ;; - stop) - Stop_Docker_Container "${CONTAINER_USER}" "${HTTP_PORT}" "${2:-approov-protected-server}" - exit 0 - ;; - shell) - Create_Docker_Container "${2:-zsh}" "${CONTAINER_USER}" "${HTTP_PORT}" "${3:-approov-protected-server}" - exit 0 - ;; - -h | --help) - Show_Help - exit 0 - ;; - esac -done - -Show_Help diff --git a/servers/shapes-api/bin/usage-help.txt b/servers/shapes-api/bin/usage-help.txt deleted file mode 100644 index 03113ec..0000000 --- a/servers/shapes-api/bin/usage-help.txt +++ /dev/null @@ -1,39 +0,0 @@ -DOCKER STACK CLI WRAPPER - -This bash script is a wrapper around docker for easier use of the docker stack -in this project. - -Signature: - ./stack [options] - - -Usage: - ./stack - ./stack [-h | --help] [-p | --port] [-u | --user] - - -Options: - -h | --help Shows this help. - -p | --port The host port to access the docker container. - -u | --user Run the docker container under the given user name or uid. - - -Commands/Args: - build Builds the docker image for this stack: - ./stack build - - approov-protected-server Runs the approov server: - ./stack approov-protected-server - ./stack --port 5000 approov-protected-server - - original-server Runs the original server: - ./stack original-server - ./stack --port 5001 original-server - - stop Stops the docker container for the given server: - ./stack stop approov-protected-server - - shell Starts a shell in a new container: - ./stack shell - ./stack shell zsh - ./stack --port 5001 shell zsh original-server diff --git a/servers/shapes-api/configuration.js b/servers/shapes-api/configuration.js deleted file mode 100644 index 034b2f5..0000000 --- a/servers/shapes-api/configuration.js +++ /dev/null @@ -1,86 +0,0 @@ -const debug = require('debug')('approov-protected-server') -const dotenv = require('dotenv').config() - -if (dotenv.error) { - debug('FAILED TO PARSE `.env` FILE | ' + dotenv.error) -} - - -///////////////////////// -/// SERVER ENVIRONMENT -//////////////////////// - -const env = dotenv.parsed.ENV || 'production' -const httpProtocol = dotenv.parsed.HTTP_PROTOCOL || 'http' -const hostName = dotenv.parsed.SERVER_HOSTNAME || 'localhost' -const httpPort = dotenv.parsed.HTTP_PORT || '5000' -const url = httpProtocol + '://' + hostName -const certificatesPath = dotenv.parsed.CERTIFICATES_PATH || "/home/node/.ssl" - -const fullUrl = function(env, url, port) { - - if (env === 'production') { - return url - } - - return url + ':' + port -} - -if (env !== 'production') { - -} - -const server = { - env: env, - hostName: hostName, - httpProtocol: httpProtocol, - httpPort: httpPort, - url: url, - fullUrl: fullUrl(env, url, httpPort), - httpsEnabled: (httpProtocol === 'https'), - certificateKey: certificatesPath + "/" + hostName + ".key", - certificatePem: certificatesPath + "/" + hostName + ".pem" -} - - -/////////////////////////// -/// APPROOV ENVIRONMENT -////////////////////////// - -let isToAbortRequestOnInvalidToken = true -let isToAbortOnInvalidBinding = true -let isApproovLoggingEnabled = true -const abortRequestOnInvalidToken = dotenv.parsed.APPROOV_ABORT_REQUEST_ON_INVALID_TOKEN || 'true' -const abortOnInvalidTokenBinding = dotenv.parsed.APPROOV_ABORT_REQUEST_ON_INVALID_TOKEN_BINDING || 'true' -const approovLoggingEnabled = dotenv.parsed.APPROOV_LOGGING_ENABLED || 'true' - -if (abortRequestOnInvalidToken.toLowerCase() === 'false') { - isToAbortRequestOnInvalidToken = false -} - -if (abortOnInvalidTokenBinding.toLowerCase() === 'false') { - isToAbortOnInvalidBinding = false -} - -if (approovLoggingEnabled.toLowerCase() === 'false') { - isApproovLoggingEnabled = false -} - -const approov = { - abortRequestOnInvalidToken: isToAbortRequestOnInvalidToken, - abortRequestOnInvalidTokenBinding: isToAbortOnInvalidBinding, - approovLoggingEnabled: isApproovLoggingEnabled, - - // The Approov base64 secret must be retrieved with the Approov CLI tool - base64Secret: dotenv.parsed.APPROOV_BASE64_SECRET, -} - - -//////////////////////////// -/// EXPORT CONFIGURATION -/////////////////////////// - -module.exports = { - server, - approov, -} diff --git a/servers/shapes-api/docker-compose.yml b/servers/shapes-api/docker-compose.yml deleted file mode 100644 index 1f3fb65..0000000 --- a/servers/shapes-api/docker-compose.yml +++ /dev/null @@ -1,41 +0,0 @@ -version: "2.1" - -services: - - dev: - image: ${DOCKER_IMAGE:-approov/quickstart-nodejs-express-token-check:dev} - build: . - env_file: - - .env - working_dir: "/home/node/workspace" - command: "npm start" - volumes: - - ./:/home/node/workspace - ports: - - 127.0.0.1:${HTTP_PORT:-8002}:${HTTP_PORT:-8002} - networks: - - default - - node: - image: ${DOCKER_IMAGE:-approov/quickstart-nodejs-express-token-check} - build: . - restart: always - env_file: .env - ports: - # Expose locally for testing - - 127.0.0.1:8001:${HTTP_PORT:-8002} - networks: - - traefik - command: sh -c "npm install && npm start" - volumes: - - ./:/home/node/workspace - labels: - - "traefik.enable=true" - - "traefik.backend=${SHAPES_NODEJS_EXPRESS_DOMAIN? Missing value for: SHAPES_NODEJS_EXPRESS_DOMAIN}" - - "traefik.docker.network=traefik" - - "traefik.port=${HTTP_PORT? Missing value for: HTTP_PORT}" - - "traefik.frontend.rule=Host:${SHAPES_NODEJS_EXPRESS_DOMAIN}" - -networks: - traefik: - external: true diff --git a/servers/shapes-api/docs/DEPLOYMENT.md b/servers/shapes-api/docs/DEPLOYMENT.md deleted file mode 100644 index fc10e2b..0000000 --- a/servers/shapes-api/docs/DEPLOYMENT.md +++ /dev/null @@ -1,62 +0,0 @@ -# DEPLOYMENT - -Guide to deploy the NodeJS Express backend into a production demo server. - -For now this will be a small set of manual steps, but later we may want to automate this via the CI pipeline, by building the docker image and the mobile app binary for the release. - -## CLONE - -```bash -git clone https://github.com/approov/quickstart-nodejs-express-token-check.git -cd quickstart-nodejs-express-token-check/servers/shapes-api -``` - -## ENVIRONMENT - -Copy the `.env.example`: - -```bash -cp .env.example .env -``` - -### The Approov secret - -The `v2/*` endpoints are protected by the Approov Token, thus we need to set the Approov secret for `nodejs-express-shapes.approov.io`. - -From your office computer, not the server, get the Approov secret with: - -```bash -approov secret -get base64 -``` - -Add it to the `.env` file: - -```bash -APPROOV_BASE64_SECRET=approov-base64-encoded-secret-here -``` - -## HOW TO RUN - -### Build the Docker Container - -```bash -sudo docker-compose build -``` - -### Bring the API up - -```bash -sudo docker-compose up -d node -``` - -### Bring the API down - -```bash -sudo docker-compose down -``` - -### Check the Logs - -```bash -sudo docker-compose logs --follow --tail 20 -``` diff --git a/servers/shapes-api/docs/approov-shapes-api-server.md b/servers/shapes-api/docs/approov-shapes-api-server.md deleted file mode 100644 index 823648d..0000000 --- a/servers/shapes-api/docs/approov-shapes-api-server.md +++ /dev/null @@ -1,476 +0,0 @@ -# APPROOV SHAPES API SERVER - -The Approov Shapes Demo Server contains endpoints with and without the Approov -protection. The protected endpoints differ in the sense that they can use or not -the optional token binding feature for the Approov token. - -We will demonstrate how to call each API endpoint with screen-shots from Postman -and from the shell. Postman is used here as an easy way to demonstrate -how you can play with the Approov integration in the API server, but to see a -real demo of how Approov would work in production you need to request a demo -[here](https://info.approov.io/demo). - -When presenting the screen-shots we will show them as 2 distinct views. The -Postman view will tell how we performed the request and what response we got -back and the shell view show us the log entries that lets us see the result of -checking the Approov token and how the requested was handled. - - -## REQUIREMENTS - -* Docker or NodeJS. -* Postman - to simulate calls to the the API server. - -## SETUP - -### Postman - -To make the API request against the Shapes API server running on your machine you will need to use Postman and import [this collection](https://raw.githubusercontent.com/approov/postman-collections/master/quickstarts/shapes-api/shapes-api.postman_collection.json) that contains all the API endpoints prepared with all scenarios we want to demonstrate. - -### Clone the Repo - -To run the Shapes API server on localhost you will need to have the repos for this demo on your machine. - -Clone from Github with: - -```bash -git clone https://github.com/approov/quickstart-nodejs-express-token-check/.git -cd quickstart-nodejs-express-token-check/servers/shapes-api -``` - -### The Environment File - -Lets' copy the `.env.example` to `.env` with the command: - -```bash -cp .env.example .env -``` - -No modifications are necessary to the newly created `.env` in order to start running the demo. - - -### Docker Stack - -In order to have an agnostic development environment through this tutorial we recommend the use of Docker, that can be installed by following [the official instructions](https://docs.docker.com/install/) for your platform, but feel free to use your own setup, provided it satisfies the [requirements](#requirements). - -A symlink `./stack` to the bash script `./bin/stack.bash` is provided in the root of the demo, at `/servers/shapes-api`, to make easy to use the docker stack to run this demo. - -Show the usage help by running from `/servers/shapes-api`: - -```bash -./stack --help -``` - -#### Building the docker image - -From your machine terminal run: - -```bash -./stack build -``` -> The image will contain the Shapes Demo Server in NodeJS. - -#### Getting a bash shell inside the docker container - -Unless you choose to not follow this demo with the provided docker stack you need to get a shell inside the docker container in order to run all the subsequent shell commands that you will be instructed to execute during the demo. - -From your machine terminal execute: - -```bash -./stack shell -``` - -#### Installing dependencies - -From the docker container shell execute: - -```bash -npm install -``` - -## RUNNING THE APPROOV SHAPES DEMO SERVER - -We will run this demo first with Approov enabled and a second time with Approov -disabled. When Approov is enabled any API endpoint protected by an Approov token -will have the request denied with a `400` or `401` response. When Approov is -disabled the check still takes place but no requests are denied, only the reason -for the failure is logged. - -### The logs - -When a request is issued from Postman you can see the logs being printed to your -shell and you can search for `approov-protected-server` to see all log entries -about requests protected by Approov and compare the logged messages with the -results returned to Postman for failures or success in the validation of -requests protected by Approov. - -An example for an accepted request: - -``` -approov-protected-server 200 GET /v2/forms ACCEPTED REQUEST WITH VALID APPROOV TOKEN +2m -approov-protected-server 200 GET /v2/forms ACCEPTED REQUEST WITH VALID APPROOV TOKEN BINDING +1ms -``` - -Examples for rejected requests: - -``` -approov-protected-server 200 GET /v2/forms ACCEPTED REQUEST WITH VALID APPROOV TOKEN +37s -approov-protected-server 200 GET /v2/forms APPROOV TOKEN BINDING ERROR: token binding in header doesn't match with the key 'pay' in the decoded token. +1ms -approov-protected-server 401 GET /v2/forms REJECTED REQUEST WITH INVALID APPROOV TOKEN BINDING +0ms -``` - -### Starting the NodeJS Express server - -Before we start the server we will want to setup the debug level to be used across all restarts. - -From the docker container shell run: - -```bash -export DEBUG=approov-protected-server -``` - -To start the server we want to issue the command: - -```bash -npm start -``` - -### Endpoint Not Protected by Approov - -This endpoint does not benefit from Approov protection and the goal here is to -show that both Approov protected and unprotected endpoints can coexist in the -same API server. - -#### /v2/hello - -**Postman View:** - -![postman hello endpoint](./assets/img/postman-hello.png) -> As we can see we have not set any headers. - -**Shell view:** - -![shell hello endpoint](./assets/img/shell-hello.png) -> As expected the logs don't have entries with Approov errors. - - -**Request Overview:** - -Looking into the Postman view, we can see that the request was sent without the -`Approov-Token` header and we got a `200` response that matches the one in the -logs output from the shell view. - - -### Endpoints Protected by an Approov Token - -The endpoints here will require an `Approov-Token` header and depending on the boolean -value for the environment variable `APPROOV_ABORT_REQUEST_ON_INVALID_TOKEN` we will -have 2 distinct behaviors. When being set to `true` we refuse to fulfill the -request and when set to `false` we will let the request pass through. For both -behaviors we log the result of checking the Approov token, but only if the environment -variable `APPROOV_LOGGING_ENABLED` is set to `true`. - -The default behavior is to have `APPROOV_ABORT_REQUEST_ON_INVALID_TOKEN` set to -`true`, but you may feel more comfortable to have it set to `false` during -the initial deployment, until you are confident that you are only refusing bad -requests to your API server. - -#### /v2/shapes - missing the Approov token header - -Make sure that the `.env` file contains `APPROOV_ABORT_REQUEST_ON_INVALID_TOKEN` set to `true`. - -Cancel current server session with `ctrl+c` and start it again with: - -```bash -npm start -``` - -**Postman view:** - -![Postman - shapes endpoint without an Approov token](./assets/img/postman-shapes-missing-approov-token.png) -> As we can see we have not set any headers. - -**Shell view:** - -![Shell - shapes endpoint without an Approov token](./assets/img/shell-shapes-missing-approov-token.png) -> As expected status code in the logs matches the one in the Postman response. - -**Request Overview:** - -Looking to the Postman view we can see that we forgot to add the `Approov-Token` -header, thus a `400` response is returned. - -In the shell view we can see in the logs entries that Approov is enabled and the Approov token is empty and this is the reason why the `400` response was -returned to Postman. - -**Let's see the same request with Approov disabled** - -Make sure that the `.env` file contains `APPROOV_ABORT_REQUEST_ON_INVALID_TOKEN` set to `false`. - -Cancel current server session with `ctrl+c` and start it again with: - -```bash -npm start -``` - -**Postman view:** - -![Postman - shapes endpoint without an Approov token and approov disabled](./assets/img/postman-shapes-missing-approov-token-and-approov-disabled.png) -> Did you notice that now we have a successfully response back? - -**Shell view:** - -![Shell - shapes endpoint without an Approov token and approov disabled](./assets/img/shell-shapes-missing-approov-token-and-approov-disabled.png) -> Can you see where are the new log entries? - -**Request Overview:** - -We continue to not provide the `Approov-Token` header but this time we have a -`200` response with the value for the shape, but once Approov is disabled the -request is not denied. - -Looking into the shell view we can see that the logs continue to tell us that -the JWT token is empty, but now we can see a log entry for the `/v2/shapes` -endpoint response with the status code `200`, meaning that the request was -fulfilled and a successful response sent back. - - -#### /v2/shapes - Malformed Approov token header - -Make sure that the `.env` file contains `APPROOV_ABORT_REQUEST_ON_INVALID_TOKEN` set to `true`. - -Cancel current server session with `ctrl+c` and start it again with: - -```bash -npm start -``` - -**Postman view:** - -![Postman - shapes endpoint with a malformed Approov token](./assets/img/postman-shapes-malformed-approov-token.png) -> Did you notice the `Approov-Token` with an invalid JWT token? - -**Shell view:** - -![Shell - shapes endpoint with a malformed Approov token](./assets/img/shell-shapes-malformed-approov-token.png) -> Can you spot what is the reason for the `401` response? - -**Request Overview:** - -In Postman we issue the request with a malformed `Approov-Token` header, that is -a normal string, not a JWT token, thus we get back a `401` response. - -Looking to shell view we can see that the logs is also telling us that the -request was denied with a `401` and that the reason is an invalid JWT token, -that doesn't contain enough segments. - - -**Let's see the same request with Approov disabled** - -Make sure that the `.env` file contains `APPROOV_ABORT_REQUEST_ON_INVALID_TOKEN` set to `false`. - -Cancel current server session with `ctrl+c` and start it again with: - -```bash -npm start -``` - -**Postman view:** - -![Postman - shapes endpoint with a malformed Approov token and approov disabled](./assets/img/postman-shapes-malformed-approov-token-and-approov-disabled.png) - - -**Shell view:** - -![Shell - shapes endpoint with a malformed Approov token and approov disabled](./assets/img/shell-shapes-malformed-approov-token-and-approov-disabled.png) - - -**Request Overview:** - -In Postman, instead of sending a valid JWT token, we continue to send the -`Approov-Token` header as a normal string, but this time we got a `200` response -back because Approov is disabled, thus not blocking the request. - -In the shell view we continue to see the same reason for the Approov token -validation failure and we can confirm the `200` response as Postman shows. - - -#### /v2/shapes - Valid Approov token header - -Make sure that the `.env` file contains `APPROOV_ABORT_REQUEST_ON_INVALID_TOKEN` set to `true`. - -Cancel current server session with `ctrl+c` and start it again with: - -```bash -npm start -``` - -> **NOTE**: -> -> For your convenience the Postman collection includes a token that only expires -> in a very distant future for this call "Approov Token with valid signature and -> expire time". For the call "Expired Approov Token with valid signature" an -> expired token is also included. - - -**Postman view with token correctly signed and not expired token:** - -![Postman - shapes endpoint with a valid Approov token](./assets/img/postman-shapes-valid-approov-token.png) - -**Postman view with token correctly signed but this time is expired:** - -![Postman - shapes endpoint with a expired Approov token](./assets/img/postman-shapes-expired-approov-token.png) - - -**Shell view:** - -![Shell - shapes endpoint with a valid and with a expired Approov token](./assets/img/shell-shapes-valid-and-expired-approov-token.png) - - -**Request Overview:** - -We used an helper script to generate an Approov Token that was valid for 1 -minute. - -In Postman we performed 2 requests with the same token and the first one was -successful, but the second request, performed 2 minutes later, failed with a -`400` response because the token have already expired as we can see by the -log messages in the shell view. - - -**Let's see the same request with Approov disabled** - -Make sure that the `.env` file contains `APPROOV_ABORT_REQUEST_ON_INVALID_TOKEN` set to `false`. - -Cancel current server session with `ctrl+c` and start it again with: - -```bash -npm start -``` -**Postman view with token valid for 1 minute:** - -![Postman - shapes endpoint with a valid Approov token and Approov disabled](./assets/img/postman-shapes-valid-approov-token-and-approov-disabled.png) - -**Postman view with same token but this time is expired:** - -![Postman - shapes endpoint with a expired Approov token and Approov disabled](./assets/img/postman-shapes-expired-approov-token-and-approov-disabled.png) - -**Shell view:** - -![Shell - shapes endpoints with a valid and with an expired Approov token and Approov disabled](./assets/img/shell-shapes-approov-disabled-with-valid-and-expired-approov-token.png) -> Can you spot where is the difference between this shell view and the previous -> one? - -**Request Overview:** - -We repeated the process to generate the Approov token with 1 minute of expiration -time. - -Once more we performed the 2 requests with the same token and with 2 minutes -interval between them but this time we got both of them with `200` responses. - -If we look into the shell view we can see that the first request have -a valid token and in the second request the token is not valid because is -expired, but once Approov is disabled the request is accepted. - -### Endpoints Protected with the Approov Token Binding - -The token binding is optional in any Approov token and you can read more about them [here](./../README.md#approov-validation-process). - -The requests where the Approov token binding is checked will be rejected on failure, but -only if the environment variable `APPROOV_ABORT_REQUEST_ON_INVALID_TOKEN_BINDING` -is set to `true`. To bear in mind that before this check is done the request -have already been through the same flow we have described for the `/v2/shapes` endpoint. - - -#### /v2/forms - Invalid Approov Token Binding - -Make sure that the `.env` file contains `APPROOV_ABORT_REQUEST_ON_INVALID_TOKEN_BINDING` set to `true`. - -Cancel current server session with `ctrl+c` and start it again with: - -```bash -npm start -``` - -**Postman view:** - -![Postman - forms endpoint with an invalid Approov token binding](./assets/img/postman-forms-invalid-approov-token-binding.png) - -**Shell view:** - -![Shell - forms endpoint with an invalid Approov token binding](./assets/img/shell-forms-invalid-approov-token-binding.png) - -**Request Overview:** - -In Postman we added an Approov token with a token binding not matching the -`Authorization` token, thus the API server rejects the request with a `401` response. - -While we can see in the shell view that the request is accepted for the Approov -token itself, afterwards we see the request being rejected, and this is due to -an invalid token binding in the Approov token, thus returning a `401` response. - -> **IMPORTANT**: -> -> When decoding the Approov token we only check if the signature and expiration -> time are valid, nothing else within the token is checked. -> -> The token binding check works on the decoded Approov token to validate if the -> value from the key `pay` matches the one for the token binding header, that in -> our case is the `Authorization` header. - - -**Let's see the same request with Approov disabled** - -Make sure that the `.env` file contains `APPROOV_ABORT_REQUEST_ON_INVALID_TOKEN_BINDING` set to `false`. - -Cancel current server session with `ctrl+c` and start it again with: - -```bash -npm start -``` - -**Postman view:** - -![Postman - forms endpoint with an invalid Approov token binding](./assets/img/postman-forms-invalid-approov-token-binding-with-approov-disabled.png) - -**Shell view:** - -![Shell - forms endpoint with an invalid Approov token binding](./assets/img/shell-forms-invalid-approov-token-binding-with-approov-disabled.png) - -**Request Overview:** - -We still have the invalid token binding in the Approov token, but once we have -disabled Approov we now have a `200` response. - -In the shell view we can confirm that the log entry still reflects that the -token binding is invalid, but this time a `200` response is logged instead of -the previously `401` one, and this is because Approov is now disabled. - - -#### /v2/forms - Valid Approov Token Binding - -Make sure that the `.env` file contains `APPROOV_ABORT_REQUEST_ON_INVALID_TOKEN_BINDING` set to `true`. - -Cancel current server session with `ctrl+c` and start it again with: - -```bash -npm start -``` - -**Postman view:** - -![Postman - forms endpoint with valid Approov Token Binding](./assets/img/postman-forms-valid-approov-token-binding.png) - -**Shell view:** - -![Shell - forms endpoint with valid Approov Token Binding](./assets/img/shell-forms-valid-approov-token-binding.png) - -**Request Overview:** - -In the Postman view the `Approov-Token` contains a valid token binding, the -`Authorization` token value, thus when we perform the request, the API server -doesn't reject it, and a `200` response is sent back. - -The shell view confirms us that the token binding is valid and we can also see -the log entry confirming the `200` response. diff --git a/servers/shapes-api/docs/assets/img/.gitkeep b/servers/shapes-api/docs/assets/img/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/servers/shapes-api/docs/assets/img/postman-forms-invalid-approov-token-binding-with-approov-disabled.png b/servers/shapes-api/docs/assets/img/postman-forms-invalid-approov-token-binding-with-approov-disabled.png deleted file mode 100644 index c2edac3..0000000 Binary files a/servers/shapes-api/docs/assets/img/postman-forms-invalid-approov-token-binding-with-approov-disabled.png and /dev/null differ diff --git a/servers/shapes-api/docs/assets/img/postman-forms-invalid-approov-token-binding.png b/servers/shapes-api/docs/assets/img/postman-forms-invalid-approov-token-binding.png deleted file mode 100644 index a74d5e7..0000000 Binary files a/servers/shapes-api/docs/assets/img/postman-forms-invalid-approov-token-binding.png and /dev/null differ diff --git a/servers/shapes-api/docs/assets/img/postman-forms-valid-approov-token-binding.png b/servers/shapes-api/docs/assets/img/postman-forms-valid-approov-token-binding.png deleted file mode 100644 index 9f3c8be..0000000 Binary files a/servers/shapes-api/docs/assets/img/postman-forms-valid-approov-token-binding.png and /dev/null differ diff --git a/servers/shapes-api/docs/assets/img/postman-hello.png b/servers/shapes-api/docs/assets/img/postman-hello.png deleted file mode 100644 index e5c0b7c..0000000 Binary files a/servers/shapes-api/docs/assets/img/postman-hello.png and /dev/null differ diff --git a/servers/shapes-api/docs/assets/img/postman-shapes-expired-approov-token-and-approov-disabled.png b/servers/shapes-api/docs/assets/img/postman-shapes-expired-approov-token-and-approov-disabled.png deleted file mode 100644 index d93f909..0000000 Binary files a/servers/shapes-api/docs/assets/img/postman-shapes-expired-approov-token-and-approov-disabled.png and /dev/null differ diff --git a/servers/shapes-api/docs/assets/img/postman-shapes-expired-approov-token.png b/servers/shapes-api/docs/assets/img/postman-shapes-expired-approov-token.png deleted file mode 100644 index 75af44f..0000000 Binary files a/servers/shapes-api/docs/assets/img/postman-shapes-expired-approov-token.png and /dev/null differ diff --git a/servers/shapes-api/docs/assets/img/postman-shapes-malformed-approov-token-and-approov-disabled.png b/servers/shapes-api/docs/assets/img/postman-shapes-malformed-approov-token-and-approov-disabled.png deleted file mode 100644 index b6de92a..0000000 Binary files a/servers/shapes-api/docs/assets/img/postman-shapes-malformed-approov-token-and-approov-disabled.png and /dev/null differ diff --git a/servers/shapes-api/docs/assets/img/postman-shapes-malformed-approov-token.png b/servers/shapes-api/docs/assets/img/postman-shapes-malformed-approov-token.png deleted file mode 100644 index 40c33dc..0000000 Binary files a/servers/shapes-api/docs/assets/img/postman-shapes-malformed-approov-token.png and /dev/null differ diff --git a/servers/shapes-api/docs/assets/img/postman-shapes-missing-approov-token-and-approov-disabled.png b/servers/shapes-api/docs/assets/img/postman-shapes-missing-approov-token-and-approov-disabled.png deleted file mode 100644 index 8c22751..0000000 Binary files a/servers/shapes-api/docs/assets/img/postman-shapes-missing-approov-token-and-approov-disabled.png and /dev/null differ diff --git a/servers/shapes-api/docs/assets/img/postman-shapes-missing-approov-token.png b/servers/shapes-api/docs/assets/img/postman-shapes-missing-approov-token.png deleted file mode 100644 index ddf6ebb..0000000 Binary files a/servers/shapes-api/docs/assets/img/postman-shapes-missing-approov-token.png and /dev/null differ diff --git a/servers/shapes-api/docs/assets/img/postman-shapes-valid-approov-token-and-approov-disabled.png b/servers/shapes-api/docs/assets/img/postman-shapes-valid-approov-token-and-approov-disabled.png deleted file mode 100644 index e03a8a8..0000000 Binary files a/servers/shapes-api/docs/assets/img/postman-shapes-valid-approov-token-and-approov-disabled.png and /dev/null differ diff --git a/servers/shapes-api/docs/assets/img/postman-shapes-valid-approov-token.png b/servers/shapes-api/docs/assets/img/postman-shapes-valid-approov-token.png deleted file mode 100644 index 2033c96..0000000 Binary files a/servers/shapes-api/docs/assets/img/postman-shapes-valid-approov-token.png and /dev/null differ diff --git a/servers/shapes-api/docs/assets/img/shell-forms-invalid-approov-token-binding-with-approov-disabled.png b/servers/shapes-api/docs/assets/img/shell-forms-invalid-approov-token-binding-with-approov-disabled.png deleted file mode 100644 index 8a73f8a..0000000 Binary files a/servers/shapes-api/docs/assets/img/shell-forms-invalid-approov-token-binding-with-approov-disabled.png and /dev/null differ diff --git a/servers/shapes-api/docs/assets/img/shell-forms-invalid-approov-token-binding.png b/servers/shapes-api/docs/assets/img/shell-forms-invalid-approov-token-binding.png deleted file mode 100644 index 7954f12..0000000 Binary files a/servers/shapes-api/docs/assets/img/shell-forms-invalid-approov-token-binding.png and /dev/null differ diff --git a/servers/shapes-api/docs/assets/img/shell-forms-valid-approov-token-binding.png b/servers/shapes-api/docs/assets/img/shell-forms-valid-approov-token-binding.png deleted file mode 100644 index 5dcf659..0000000 Binary files a/servers/shapes-api/docs/assets/img/shell-forms-valid-approov-token-binding.png and /dev/null differ diff --git a/servers/shapes-api/docs/assets/img/shell-hello.png b/servers/shapes-api/docs/assets/img/shell-hello.png deleted file mode 100644 index 86eb7f1..0000000 Binary files a/servers/shapes-api/docs/assets/img/shell-hello.png and /dev/null differ diff --git a/servers/shapes-api/docs/assets/img/shell-shapes-approov-disabled-with-valid-and-expired-approov-token.png b/servers/shapes-api/docs/assets/img/shell-shapes-approov-disabled-with-valid-and-expired-approov-token.png deleted file mode 100644 index d8028ff..0000000 Binary files a/servers/shapes-api/docs/assets/img/shell-shapes-approov-disabled-with-valid-and-expired-approov-token.png and /dev/null differ diff --git a/servers/shapes-api/docs/assets/img/shell-shapes-malformed-approov-token-and-approov-disabled.png b/servers/shapes-api/docs/assets/img/shell-shapes-malformed-approov-token-and-approov-disabled.png deleted file mode 100644 index eab72d8..0000000 Binary files a/servers/shapes-api/docs/assets/img/shell-shapes-malformed-approov-token-and-approov-disabled.png and /dev/null differ diff --git a/servers/shapes-api/docs/assets/img/shell-shapes-malformed-approov-token.png b/servers/shapes-api/docs/assets/img/shell-shapes-malformed-approov-token.png deleted file mode 100644 index 23ed11a..0000000 Binary files a/servers/shapes-api/docs/assets/img/shell-shapes-malformed-approov-token.png and /dev/null differ diff --git a/servers/shapes-api/docs/assets/img/shell-shapes-missing-approov-token-and-approov-disabled.png b/servers/shapes-api/docs/assets/img/shell-shapes-missing-approov-token-and-approov-disabled.png deleted file mode 100644 index dae2af4..0000000 Binary files a/servers/shapes-api/docs/assets/img/shell-shapes-missing-approov-token-and-approov-disabled.png and /dev/null differ diff --git a/servers/shapes-api/docs/assets/img/shell-shapes-missing-approov-token.png b/servers/shapes-api/docs/assets/img/shell-shapes-missing-approov-token.png deleted file mode 100644 index 40da178..0000000 Binary files a/servers/shapes-api/docs/assets/img/shell-shapes-missing-approov-token.png and /dev/null differ diff --git a/servers/shapes-api/docs/assets/img/shell-shapes-valid-and-expired-approov-token.png b/servers/shapes-api/docs/assets/img/shell-shapes-valid-and-expired-approov-token.png deleted file mode 100644 index 14eb772..0000000 Binary files a/servers/shapes-api/docs/assets/img/shell-shapes-valid-and-expired-approov-token.png and /dev/null differ diff --git a/servers/shapes-api/index.html b/servers/shapes-api/index.html deleted file mode 100644 index 294bb52..0000000 --- a/servers/shapes-api/index.html +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - -

Approov Mobile App Authentication

-

- To learn more about how Approov protects your APIs from malicious bots and tampered or fake apps, see - https://approov.io. -

- - diff --git a/servers/shapes-api/original-server.js b/servers/shapes-api/original-server.js deleted file mode 100644 index 511791d..0000000 --- a/servers/shapes-api/original-server.js +++ /dev/null @@ -1,114 +0,0 @@ -const debug = require('debug')('original-server') -const config = require('./configuration') -const https = require('https') -const fs = require('fs') -const express = require('express') -const cors = require('cors') -const path = require('path') -const app = express() -app.use(cors()) - - -//////////////// -/// FUNCTIONS -//////////////// - -const buildLogMessagePrefix = function(req, res) { - return res.statusCode + ' ' + req.method + ' ' + req.originalUrl -} - -const logResponseToRequest = function(req, res) { - debug(buildLogMessagePrefix(req, res)) -} - -const getRandomShapeResponse = function() { - const shapes = [ - 'Circle', - 'Triangle', - 'Square', - 'Rectangle' - ] - return { - shape: shapes[Math.floor((Math.random() * shapes.length))] - } -} - -const getRandomFormResponse = function() { - const forms = [ - 'Sphere', - 'Cone', - 'Cube', - 'Box' - ] - return {"form": forms[Math.floor((Math.random() * forms.length))]} -} - -const buildHelloWorldResponse = function(res) { - res.json({ - text: "Hello, World!", - }) -} - -const buildShapesResponse = function(res, protectionStatus) { - const response = getRandomShapeResponse() - res.json(response) -} - -const buildFormsResponse = function(res, protectionStatus) { - const response = getRandomFormResponse() - res.json(response) -} - - -//////////////// -// ENDPOINTS -//////////////// - -/** - * V1 ENDPOINTS - */ - -// simple 'hello world' endpoint. -app.get('/v1/hello', function (req, res, next) { - logResponseToRequest(req, res) - buildHelloWorldResponse(res) -}) - -// shapes endpoint returns a random shape. -app.get('/v1/shapes', function(req, res, next) { - logResponseToRequest(req, res) - buildShapesResponse(res, 'unprotected') -}) - -// shapes endpoint returns a random form. -app.get('/v1/forms', function(req, res, next) { - logResponseToRequest(req, res) - buildFormsResponse(res, 'unprotected') -}) - - -//////////// -// SERVER -//////////// - -if (config.server.httpsEnabled) { - // Load the certificate and key data for our server to be hosted over HTTPS - const serverOptions = { - key: fs.readFileSync(config.server.certificateKey), - cert: fs.readFileSync(config.server.certificatePem), - requestCert: false, - rejectUnauthorized: false - } - - // Create and run the HTTPS server - https.createServer(serverOptions, app).listen(config.server.httpPort, function() { - debug("Shapes server listening on %s", config.server.fullUrl) - }) - -} else { - - // Create and run the HTTP server - app.listen(config.server.httpPort, function () { - debug("Shapes server listening on %s", config.server.fullUrl) - }) -} diff --git a/servers/shapes-api/package.json b/servers/shapes-api/package.json deleted file mode 100644 index ce55dea..0000000 --- a/servers/shapes-api/package.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "name": "demo-shapes-server", - "version": "1.0.0", - "description": "Node Express version of the Approov demo shapes server.", - "main": "approov-protected-server.js", - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1", - "start": "nodemon approov-protected-server.js", - "original-server": "nodemon original-server.js" - }, - "author": "CriticalBlue.com", - "license": "Apache-2.0", - "dependencies": { - "cors": "^2.8.5", - "debug": "^4.3.6", - "dotenv": "^16.4.5", - "express": "^4.19.2", - "express-jwt": "^8.4.1", - "fs": "0.0.1-security" - }, - "devDependencies": { - "minimist": "^1.2.8", - "nodemon": "^3.1.4" - } -} diff --git a/servers/shapes-api/stack b/servers/shapes-api/stack deleted file mode 120000 index 1b077ec..0000000 --- a/servers/shapes-api/stack +++ /dev/null @@ -1 +0,0 @@ -bin/stack.bash \ No newline at end of file diff --git a/test.sh b/test.sh new file mode 100644 index 0000000..a5b8b28 --- /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", "SessionId"]. + local AUTH_VAL2="ExampleAuthToken==" + local SI_VAL="123" + export HASH_INPUT="${AUTH_VAL2}${SI_VAL}" + + gen_token \ + "${TOKDIR}/approov_token_bind_auth_si_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 "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_si_valid")" \ + "${BASE_URL}/token-double-binding" + + # 3.3) Incorrect headers. + run_test \ + "Double Binding - incorrect binding headers" \ + "${failure_code}" \ + -H "Authorization: BadAuthToken==" \ + -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_si_invalid" \ + -setDataHashInToken "${HASH_INPUT}" \ + -genExample \ + example.com \ + -type invalid || true + + run_test \ + "Double Binding - invalid token" \ + "${failure_code}" \ + -H "Authorization: ${AUTH_VAL2}" \ + -H "SessionId: ${SI_VAL}" \ + -H "approov-token: $(<"${TOKDIR}/approov_token_bind_auth_si_invalid")" \ + "${BASE_URL}/token-double-binding" + + echo + echo "Full request and response details are saved in:" + echo " ${LOGFILE}" +} + +main "$@"