From 4800754d8a23429cf6406915d42fc937ae9404d4 Mon Sep 17 00:00:00 2001 From: Samuel Pull Date: Thu, 21 Nov 2024 11:21:23 +0100 Subject: [PATCH 1/4] api: configurable refresh and access tokens --- api/environment-variables.md | 6 ++++-- api/src/authenticationUtils.ts | 8 ++++---- api/src/config.ts | 6 ++++++ api/src/envVarsSchema.ts | 10 ++++++++++ api/src/user_authenticate.ts | 4 ++-- api/src/user_authenticateAd.ts | 4 ++-- scripts/development/docker-compose.yml | 2 ++ scripts/operation/docker-compose.yml | 2 ++ 8 files changed, 32 insertions(+), 10 deletions(-) diff --git a/api/environment-variables.md b/api/environment-variables.md index 5083d3d91..14d718d13 100644 --- a/api/environment-variables.md +++ b/api/environment-variables.md @@ -8,7 +8,7 @@ | **ORGANIZATION** | no | MyOrga | In the blockchain network, each node is represented by its organization name. This environment variable sets this organization name. It is used to create the organization stream on the blockchain and is also displayed in the frontend's top right corner. Minimal value: 1. Maximal value: 100. | | **PORT** | no | 8091 | The port used to expose the API for your installation. Example: If you run TruBudget locally and set API_PORT to `8080`, you can reach the API via `localhost:8080/api`. Value is a port with minimal value 0 and maximal value 65535 | | **ORGANIZATION_VAULT_SECRET** | yes | - | This is the key to en-/decrypt user data of an organization. If you want to add a new node for your organization, you want users to be able to log in on either node. **Caution:** If you want to run TruBudget in production, make sure NOT to use the default value from the `.env.example` file! Minimal value: 5. | -| **ROOT_SECRET** | no | 8d0ab15d21b6d2c48d834bad4785a52126573906c84e6120506fce35f5ce4708 | The root secret is the password for the root user. If you start with an empty blockchain, the root user is needed to add other users, approve new nodes,.. If you don't set a value via the environment variable, the API generates one randomly and prints it to the console **Caution:** If you want to run TruBudget in production, make sure to set a secure root secret. Minimal value: 8. | +| **ROOT_SECRET** | no | 95d002575da0517b7822253314971fc51b8ffe8df9829086321807e6d90dd1a4 | The root secret is the password for the root user. If you start with an empty blockchain, the root user is needed to add other users, approve new nodes,.. If you don't set a value via the environment variable, the API generates one randomly and prints it to the console **Caution:** If you want to run TruBudget in production, make sure to set a secure root secret. Minimal value: 8. | | **MULTICHAIN_RPC_HOST** | no | localhost | The IP address of the blockchain (not multichain daemon,but they are usally the same) you want to connect to. | | **MULTICHAIN_RPC_PORT** | no | 8000 | The Port of the blockchain where the server is available for incoming http connections (e.g. readiness, versions, backup and restore) | | **MULTICHAIN_PROTOCOL** | no | http | The protocol used to expose the multichain daemon of your Trubudget blockchain installation(bc). The protocol used to connect to the multichain daemon(api). This will be used internally for the communication between the API and the multichain daemon. | @@ -18,7 +18,7 @@ | **BLOCKCHAIN_PROTOCOL** | no | http | The Protocol of the blockchain where the server is available for incoming http connections. | | **SWAGGER_BASEPATH** `deprecated` | no | - | deprecated This variable was used to choose which environment (prod or test) is used for testing the requests. The variable is deprecated now, as the Swagger documentation can be used for the prod and test environment separately. Example values: "/". | | **JWT_ALGORITHM** | no | HS256 | Algorithm used for signing and verifying JWTs. | -| **JWT_SECRET** | yes (if JWT_ALGORITHM=RS256) | 9640f5d6c8af1bc72275 | A string that is used to sign JWT which are created by the authenticate endpoint of the api. If JWT_ALGORITHM is set to `RS256`, this is required and holds BASE64 encoded PEM encoded private key for RSA. | +| **JWT_SECRET** | yes (if JWT_ALGORITHM=RS256) | d842248e4fd8cc95267d | A string that is used to sign JWT which are created by the authenticate endpoint of the api. If JWT_ALGORITHM is set to `RS256`, this is required and holds BASE64 encoded PEM encoded private key for RSA. | | **JWT_PUBLIC_KEY** | yes (if JWT_ALGORITHM=RS256) | - | If JWT_ALGORITHM is set to `RS256`, this is required and holds BASE64 encoded PEM encoded public key for RSA. | | **DOCUMENT_FEATURE_ENABLED** | no | - | If true, all uploaded documents are stored using trubudget's storage-service. If false, the document feature of TruBudget is disabled, and trying to upload a document will result in an error. | | **DOCUMENT_EXTERNAL_LINKS_ENABLED** | no | - | If true, it is possible to use external documents links also without TruBudget's storage-service. If false, the external documents links feature of TruBudget is still possible to use in case DOCUMENT_FEATURE_ENABLED equals "true". | @@ -38,6 +38,8 @@ | **AUTHPROXY_JWS_SIGNATURE** | yes (if AUTHPROXY_ENABLED=true) | - | secret/public key/certificate for verifying auth proxy token signature | | **DB_TYPE** | no | pg | - | | **SQL_DEBUG** | no | - | - | +| **REFRESH_TOKEN_EXPIRATION** | no | 8 | Refresh token expiration in hours | +| **ACCESS_TOKEN_EXPIRATION** | no | 15 | Access token expiration in minutes | | **REFRESH_TOKEN_STORAGE** | no | - | Determining the type of storage for refresh tokens. Allowed values are "db" or "memory" or blank to disable refresh token functionality. | | **API_DB_USER** | yes (if REFRESH_TOKEN_STORAGE=db) | postgres | Database user for database connection, e.g. postgres | | **API_DB_PASSWORD** | yes (if REFRESH_TOKEN_STORAGE=db) | test | Database password for database connection | diff --git a/api/src/authenticationUtils.ts b/api/src/authenticationUtils.ts index cf8bb731e..c9ac0c8c7 100644 --- a/api/src/authenticationUtils.ts +++ b/api/src/authenticationUtils.ts @@ -1,9 +1,9 @@ import * as jsonwebtoken from "jsonwebtoken"; -import { JwtConfig } from "./config"; +import { config, JwtConfig } from "./config"; -export const refreshTokenExpirationInDays = 8; -export const accessTokenExpirationInMinutesWithrefreshToken = 10; +export const refreshTokenExpirationInHours = config.refreshTokenExpiration; +export const accessTokenExpirationInMinutesWithrefreshToken = config.accessTokenExpiration; /** * Creates a refresh JWT Token @@ -18,7 +18,7 @@ export function createRefreshJWTToken( ): string { const secretOrPrivateKey = algorithm === "RS256" ? Buffer.from(key, "base64") : key; return jsonwebtoken.sign(payload, secretOrPrivateKey, { - expiresIn: `${refreshTokenExpirationInDays}d`, + expiresIn: `${refreshTokenExpirationInHours}h`, algorithm, }); } diff --git a/api/src/config.ts b/api/src/config.ts index 5ac96d6dd..9148eb470 100644 --- a/api/src/config.ts +++ b/api/src/config.ts @@ -53,6 +53,8 @@ interface ProcessEnvVars { API_DB_PORT: string; API_DB_SSL: string; API_DB_SCHEMA: string; + REFRESH_TOKEN_EXPIRATION: string; + ACCESS_TOKEN_EXPIRATION: string; API_REFRESH_TOKENS_TABLE: string; REFRESH_TOKEN_STORAGE?: string; // "db" || "memory" || undefined } @@ -122,6 +124,8 @@ interface Config { db: DatabaseConfig; dbType: string; sqlDebug: boolean | undefined; + accessTokenExpiration: number; + refreshTokenExpiration: number; refreshTokensTable: string | undefined; refreshTokenStorage: string | undefined; snapshotEventInterval: number; @@ -198,6 +202,8 @@ export const config: Config = { }, dbType: envVars.DB_TYPE, sqlDebug: envVars.SQL_DEBUG, + accessTokenExpiration: envVars.ACCESS_TOKEN_EXPIRATION, + refreshTokenExpiration: envVars.REFRESH_TOKEN_EXPIRATION, refreshTokensTable: envVars.API_REFRESH_TOKENS_TABLE, refreshTokenStorage: envVars.REFRESH_TOKEN_STORAGE, snapshotEventInterval: envVars.SNAPSHOT_EVENT_INTERVAL, diff --git a/api/src/envVarsSchema.ts b/api/src/envVarsSchema.ts index 733bfb358..c1d2237fe 100644 --- a/api/src/envVarsSchema.ts +++ b/api/src/envVarsSchema.ts @@ -258,6 +258,16 @@ export const envVarsSchema = Joi.object({ .empty(["", null]) .default("public") .note('Database schema, e.g. "public".'), + REFRESH_TOKEN_EXPIRATION: Joi.number() + .default(8) + .allow("") + .empty(["", null]) + .note("Refresh token expiration in hours"), + ACCESS_TOKEN_EXPIRATION: Joi.number() + .default(15) + .allow("") + .empty(["", null]) + .note("Access token expiration in minutes"), API_REFRESH_TOKENS_TABLE: Joi.string() .empty(["", null]) .default("refresh_token") diff --git a/api/src/user_authenticate.ts b/api/src/user_authenticate.ts index fb58d195c..19468307d 100644 --- a/api/src/user_authenticate.ts +++ b/api/src/user_authenticate.ts @@ -6,7 +6,7 @@ import { VError } from "verror"; import { accessTokenExpirationInMinutesWithrefreshToken, createRefreshJWTToken, - refreshTokenExpirationInDays, + refreshTokenExpirationInHours, } from "./authenticationUtils"; import { JwtConfig, config } from "./config"; import { toHttpError } from "./http_errors"; @@ -252,7 +252,7 @@ export function addHttpHandler( const now = new Date(); // time in miliseconds of refresh token expiration const refreshTokenExpiration = new Date( - now.getTime() + 1000 * 60 * 60 * 24 * refreshTokenExpirationInDays, + now.getTime() + 1000 * 60 * 60 * refreshTokenExpirationInHours, ); const refreshToken = createRefreshJWTToken( { userId: token.userId, expirationAt: refreshTokenExpiration }, diff --git a/api/src/user_authenticateAd.ts b/api/src/user_authenticateAd.ts index 517515bc4..dfe80713c 100644 --- a/api/src/user_authenticateAd.ts +++ b/api/src/user_authenticateAd.ts @@ -6,7 +6,7 @@ import { VError } from "verror"; import { accessTokenExpirationInMinutesWithrefreshToken, createRefreshJWTToken, - refreshTokenExpirationInDays, + refreshTokenExpirationInHours, } from "./authenticationUtils"; import { JwtConfig, config } from "./config"; import { toHttpError } from "./http_errors"; @@ -247,7 +247,7 @@ export function addHttpHandler( const now = new Date(); // time in miliseconds of refresh token expiration const refreshTokenExpiration = new Date( - now.getTime() + 1000 * 60 * 60 * 24 * refreshTokenExpirationInDays, + now.getTime() + 1000 * 60 * 60 * refreshTokenExpirationInHours, ); const refreshToken = createRefreshJWTToken( { userId: token.userId, expirationAt: refreshTokenExpiration }, diff --git a/scripts/development/docker-compose.yml b/scripts/development/docker-compose.yml index d00c37254..67653b359 100644 --- a/scripts/development/docker-compose.yml +++ b/scripts/development/docker-compose.yml @@ -103,6 +103,8 @@ services: API_DB_SCHEMA: ${API_DB_SCHEMA} API_DB_NAME: ${API_DB_NAME} API_REFRESH_TOKENS_TABLE: ${API_REFRESH_TOKENS_TABLE} + REFRESH_TOKEN_EXPIRATION: ${REFRESH_TOKEN_EXPIRATION} + ACCESS_TOKEN_EXPIRATION: ${ACCESS_TOKEN_EXPIRATION} command: ["npm", "run", "watch"] # npm run watch: hot reloading # volume to track code changes volumes: diff --git a/scripts/operation/docker-compose.yml b/scripts/operation/docker-compose.yml index f83d75c11..7aed3387f 100644 --- a/scripts/operation/docker-compose.yml +++ b/scripts/operation/docker-compose.yml @@ -115,6 +115,8 @@ services: API_DB_SCHEMA: ${API_DB_SCHEMA} API_DB_NAME: ${API_DB_NAME} API_REFRESH_TOKENS_TABLE: ${API_REFRESH_TOKENS_TABLE} + REFRESH_TOKEN_EXPIRATION: ${REFRESH_TOKEN_EXPIRATION} + ACCESS_TOKEN_EXPIRATION: ${ACCESS_TOKEN_EXPIRATION} depends_on: - alpha-node volumes: From 9919c0483c0d75f2274a21133d31b59334ba9afd Mon Sep 17 00:00:00 2001 From: Samuel Pull Date: Thu, 21 Nov 2024 17:56:56 +0100 Subject: [PATCH 2/4] plugin wip --- api/package-lock.json | 53 ++++++++++++++++++++++++++- api/package.json | 1 + api/src/plugins/activity.ts | 35 ++++++++++++++++++ api/src/service/user_refresh_token.ts | 4 +- 4 files changed, 91 insertions(+), 2 deletions(-) create mode 100644 api/src/plugins/activity.ts diff --git a/api/package-lock.json b/api/package-lock.json index 1c618b4f3..a176ed843 100644 --- a/api/package-lock.json +++ b/api/package-lock.json @@ -31,6 +31,7 @@ "content-disposition": "^0.5.4", "fastify": "^4.28.1", "fastify-metrics": "^10.3.0", + "fastify-plugin": "^5.0.1", "joi": "^17.9.1", "jsonwebtoken": "^9.0.0", "knex": "^3.1.0", @@ -880,6 +881,11 @@ "fastify-plugin": "^4.0.0" } }, + "node_modules/@fastify/cookie/node_modules/fastify-plugin": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-4.5.1.tgz", + "integrity": "sha512-stRHYGeuqpEZTL1Ef0Ovr2ltazUT9g844X5z/zEBFLG8RYlpDiOCIG+ATvYEp+/zmc7sN29mcIMp8gvYplYPIQ==" + }, "node_modules/@fastify/cors": { "version": "8.5.0", "resolved": "https://registry.npmjs.org/@fastify/cors/-/cors-8.5.0.tgz", @@ -889,6 +895,11 @@ "mnemonist": "0.39.6" } }, + "node_modules/@fastify/cors/node_modules/fastify-plugin": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-4.5.1.tgz", + "integrity": "sha512-stRHYGeuqpEZTL1Ef0Ovr2ltazUT9g844X5z/zEBFLG8RYlpDiOCIG+ATvYEp+/zmc7sN29mcIMp8gvYplYPIQ==" + }, "node_modules/@fastify/deepmerge": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/@fastify/deepmerge/-/deepmerge-1.3.0.tgz", @@ -916,6 +927,11 @@ "helmet": "^6.0.0" } }, + "node_modules/@fastify/helmet/node_modules/fastify-plugin": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-4.5.1.tgz", + "integrity": "sha512-stRHYGeuqpEZTL1Ef0Ovr2ltazUT9g844X5z/zEBFLG8RYlpDiOCIG+ATvYEp+/zmc7sN29mcIMp8gvYplYPIQ==" + }, "node_modules/@fastify/jwt": { "version": "7.2.4", "resolved": "https://registry.npmjs.org/@fastify/jwt/-/jwt-7.2.4.tgz", @@ -928,6 +944,11 @@ "steed": "^1.1.3" } }, + "node_modules/@fastify/jwt/node_modules/fastify-plugin": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-4.5.1.tgz", + "integrity": "sha512-stRHYGeuqpEZTL1Ef0Ovr2ltazUT9g844X5z/zEBFLG8RYlpDiOCIG+ATvYEp+/zmc7sN29mcIMp8gvYplYPIQ==" + }, "node_modules/@fastify/merge-json-schemas": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/@fastify/merge-json-schemas/-/merge-json-schemas-0.1.1.tgz", @@ -949,6 +970,11 @@ "stream-wormhole": "^1.1.0" } }, + "node_modules/@fastify/multipart/node_modules/fastify-plugin": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-4.5.1.tgz", + "integrity": "sha512-stRHYGeuqpEZTL1Ef0Ovr2ltazUT9g844X5z/zEBFLG8RYlpDiOCIG+ATvYEp+/zmc7sN29mcIMp8gvYplYPIQ==" + }, "node_modules/@fastify/rate-limit": { "version": "8.1.1", "resolved": "https://registry.npmjs.org/@fastify/rate-limit/-/rate-limit-8.1.1.tgz", @@ -959,6 +985,11 @@ "tiny-lru": "^11.0.0" } }, + "node_modules/@fastify/rate-limit/node_modules/fastify-plugin": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-4.5.1.tgz", + "integrity": "sha512-stRHYGeuqpEZTL1Ef0Ovr2ltazUT9g844X5z/zEBFLG8RYlpDiOCIG+ATvYEp+/zmc7sN29mcIMp8gvYplYPIQ==" + }, "node_modules/@fastify/send": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/@fastify/send/-/send-2.1.0.tgz", @@ -984,6 +1015,11 @@ "p-limit": "^3.1.0" } }, + "node_modules/@fastify/static/node_modules/fastify-plugin": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-4.5.1.tgz", + "integrity": "sha512-stRHYGeuqpEZTL1Ef0Ovr2ltazUT9g844X5z/zEBFLG8RYlpDiOCIG+ATvYEp+/zmc7sN29mcIMp8gvYplYPIQ==" + }, "node_modules/@fastify/swagger": { "version": "8.14.0", "resolved": "https://registry.npmjs.org/@fastify/swagger/-/swagger-8.14.0.tgz", @@ -1029,6 +1065,11 @@ "balanced-match": "^1.0.0" } }, + "node_modules/@fastify/swagger-ui/node_modules/fastify-plugin": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-4.5.1.tgz", + "integrity": "sha512-stRHYGeuqpEZTL1Ef0Ovr2ltazUT9g844X5z/zEBFLG8RYlpDiOCIG+ATvYEp+/zmc7sN29mcIMp8gvYplYPIQ==" + }, "node_modules/@fastify/swagger-ui/node_modules/foreground-child": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", @@ -1090,6 +1131,11 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/@fastify/swagger/node_modules/fastify-plugin": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-4.5.1.tgz", + "integrity": "sha512-stRHYGeuqpEZTL1Ef0Ovr2ltazUT9g844X5z/zEBFLG8RYlpDiOCIG+ATvYEp+/zmc7sN29mcIMp8gvYplYPIQ==" + }, "node_modules/@grpc/grpc-js": { "version": "1.10.10", "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.10.10.tgz", @@ -4661,11 +4707,16 @@ "fastify": ">=4" } }, - "node_modules/fastify-plugin": { + "node_modules/fastify-metrics/node_modules/fastify-plugin": { "version": "4.5.1", "resolved": "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-4.5.1.tgz", "integrity": "sha512-stRHYGeuqpEZTL1Ef0Ovr2ltazUT9g844X5z/zEBFLG8RYlpDiOCIG+ATvYEp+/zmc7sN29mcIMp8gvYplYPIQ==" }, + "node_modules/fastify-plugin": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-5.0.1.tgz", + "integrity": "sha512-HCxs+YnRaWzCl+cWRYFnHmeRFyR5GVnJTAaCJQiYzQSDwK9MgJdyAsuL3nh0EWRCYMgQ5MeziymvmAhUHYHDUQ==" + }, "node_modules/fastify/node_modules/pino": { "version": "9.1.0", "resolved": "https://registry.npmjs.org/pino/-/pino-9.1.0.tgz", diff --git a/api/package.json b/api/package.json index 95ea648d0..874a3ac76 100644 --- a/api/package.json +++ b/api/package.json @@ -95,6 +95,7 @@ "content-disposition": "^0.5.4", "fastify": "^4.28.1", "fastify-metrics": "^10.3.0", + "fastify-plugin": "^5.0.1", "joi": "^17.9.1", "jsonwebtoken": "^9.0.0", "knex": "^3.1.0", diff --git a/api/src/plugins/activity.ts b/api/src/plugins/activity.ts new file mode 100644 index 000000000..2802a235a --- /dev/null +++ b/api/src/plugins/activity.ts @@ -0,0 +1,35 @@ +import { FastifyInstance, FastifyPluginOptions } from "fastify"; +import fp from "fastify-plugin"; + +import { AuthenticatedRequest } from "../httpd/lib"; + +async function activityTrackingPlugin( + fastify: FastifyInstance, + options: FastifyPluginOptions, +): Promise { + fastify.addHook("preHandler", async (request: AuthenticatedRequest, reply) => { + // if user id in request, set last activity + if (request.user) { + try { + const userId = request.user?.userId; + } catch (err) { + //logger.error({ err }, `preHandler failed to get groups for user ${request.user?.userId}`); + } + } + }); + + // background job to check idle users + const checkIdleUsers = async (): Promise => { + for (let i = 0; i < 1; i++) {} + }; + + // run job every X minutes - setInterval + const intervalId = setInterval(checkIdleUsers, 5 * 60 * 1000); + + // cleanup + fastify.addHook("onClose", () => { + clearInterval(intervalId); + }); +} + +export default fp(activityTrackingPlugin, { name: "activity-plugin" }); diff --git a/api/src/service/user_refresh_token.ts b/api/src/service/user_refresh_token.ts index 5202c939e..62a339771 100644 --- a/api/src/service/user_refresh_token.ts +++ b/api/src/service/user_refresh_token.ts @@ -4,7 +4,7 @@ import { globalIntents } from "../authz/intents"; import { config } from "../config"; import { Ctx } from "../lib/ctx"; import DbConnector from "../lib/db"; -import { getValue } from "../lib/keyValueStore"; +import { getValue, getAllValues } from "../lib/keyValueStore"; import logger from "../lib/logger"; import * as SymmetricCrypto from "../lib/symmetricCrypto"; import { verifyToken } from "../lib/token"; @@ -48,6 +48,8 @@ export async function validateRefreshToken( let storedRefreshToken: { userId: string; validUntil: number } | undefined; if (config.refreshTokenStorage === "memory") { + const storeValues = getAllValues(); + logger.error(JSON.stringify(storeValues, null, 2)); storedRefreshToken = getValue(`refreshToken.${refreshToken}`) as | { userId: string; validUntil: number } | undefined; From 5b28d6025fbc1fcb0f704c59d30735d3585fd682 Mon Sep 17 00:00:00 2001 From: Samuel Pull Date: Fri, 22 Nov 2024 11:22:29 +0100 Subject: [PATCH 3/4] kv store on steroids --- api/src/lib/keyValueStore.ts | 128 +++++++++++++++++++++----- api/src/plugins/activity.ts | 21 ++++- api/src/service/user_refresh_token.ts | 7 +- api/src/user_authenticate.ts | 4 +- api/src/user_authenticateAd.ts | 4 +- api/src/user_logout.ts | 4 +- 6 files changed, 134 insertions(+), 34 deletions(-) diff --git a/api/src/lib/keyValueStore.ts b/api/src/lib/keyValueStore.ts index 3a4bc4671..d69d33fc9 100644 --- a/api/src/lib/keyValueStore.ts +++ b/api/src/lib/keyValueStore.ts @@ -1,36 +1,116 @@ /** - * @description Simple data key/value store + * @description Typed key-value store with expiration support using Map */ -const store = {}; -export const saveValue = (key: string, value: {}, exp: Date | number): void => { - if (!key || !value) { - throw Error("key and value are required"); +interface StoreEntry { + value: T; + exp: number; +} + +class KeyValueStore { + private store: Map>; + private cleanupInterval: ReturnType; + + constructor(cleanupIntervalMs: number = 60 * 1000) { + this.store = new Map(); + this.cleanupInterval = setInterval(() => this.clearExpiredKeys(), cleanupIntervalMs); } - const expMs = exp instanceof Date ? exp.getTime() : exp; - store[key] = { ...value, exp: expMs }; -}; + /** + * Saves a value in the store with expiration + * @throws {ValidationError} If key or value is invalid + */ + public save(key: string, value: T, exp: Date | number): void { + if (!key?.trim()) { + throw new Error("Key must be a non-empty string"); + } -export const getValue = (key: string): unknown => store[key]; + if (value === undefined || value === null) { + throw new Error("Value cannot be null or undefined"); + } + + const expMs = exp instanceof Date ? exp.getTime() : exp; + + if (typeof expMs !== "number" || expMs <= Date.now()) { + throw new Error("Expiration must be a future timestamp or Date"); + } -export const clearValue = (key: string): void => { - if (store[key]) { - delete store[key]; + this.store.set(key, { value, exp: expMs }); } -}; -export const clearExpiredKeys = (): void => { - const now = new Date(); - const nowMs = now.getTime(); - Object.keys(store).forEach((key) => { - // key is expired - if (store?.[key]?.exp && nowMs > store?.[key]?.exp) { - clearValue(key); + /** + * Retrieves a value from the store + * @returns The value if found and not expired, undefined otherwise + */ + public get(key: string): T | undefined { + const entry = this.store.get(key); + if (!entry) return undefined; + + if (Date.now() > entry.exp) { + this.clear(key); + return undefined; } - }); -}; -setInterval(clearExpiredKeys, 1000 * 60); + return entry.value as T; + } + + /** + * Removes a value from the store + * @returns boolean indicating if the value was cleared + */ + public clear(key: string): boolean { + return this.store.delete(key); + } + + /** + * Clears all expired keys from the store + * @returns number of cleared keys + */ + public clearExpiredKeys(): number { + const now = Date.now(); + let cleared = 0; + + for (const [key, entry] of this.store) { + if (entry.exp <= now) { + this.store.delete(key); + cleared++; + } + } + + return cleared; + } + + /** + * Returns all non-expired values in the store + */ + public getAll(): Map { + const now = Date.now(); + const result = new Map(); + + for (const [key, entry] of this.store) { + if (entry.exp > now) { + result.set(key, entry.value); + } + } + + return result; + } + + /** + * Returns the number of entries in the store + */ + public size(): number { + return this.store.size; + } + + /** + * Stops the cleanup interval and clears all entries + */ + public dispose(): void { + clearInterval(this.cleanupInterval); + this.store.clear(); + } +} -export const getAllValues = (): any => store; +// Export a singleton instance +export const kvStore = new KeyValueStore(); diff --git a/api/src/plugins/activity.ts b/api/src/plugins/activity.ts index 2802a235a..73f005efa 100644 --- a/api/src/plugins/activity.ts +++ b/api/src/plugins/activity.ts @@ -1,8 +1,11 @@ import { FastifyInstance, FastifyPluginOptions } from "fastify"; import fp from "fastify-plugin"; +import { config } from "../config"; import { AuthenticatedRequest } from "../httpd/lib"; +import { kvStore } from "../lib/keyValueStore"; +// todo should not run in refreshtoken route async function activityTrackingPlugin( fastify: FastifyInstance, options: FastifyPluginOptions, @@ -12,6 +15,12 @@ async function activityTrackingPlugin( if (request.user) { try { const userId = request.user?.userId; + if (config.refreshTokenStorage === "memory") { + // TODO save, update last activity + kvStore.save(`lastActivity.${userId}`, Date.now(), Date.now() + 1000 * 60 * 10); + } else if (config.refreshTokenStorage === "db") { + // create or update last access for userId + } } catch (err) { //logger.error({ err }, `preHandler failed to get groups for user ${request.user?.userId}`); } @@ -20,7 +29,17 @@ async function activityTrackingPlugin( // background job to check idle users const checkIdleUsers = async (): Promise => { - for (let i = 0; i < 1; i++) {} + try { + if (config.refreshTokenStorage === "memory") { + // go through all stored lastAccesses + // invalidate refreshTokens of users with last activity older than X + } else if (config.refreshTokenStorage === "db") { + // get all last accesses + // invalidate refreshTokens of users with last activity older than X + } + } catch (err) { + //logger.error({ err }, `preHandler failed to get groups for user ${request.user?.userId}`); + } }; // run job every X minutes - setInterval diff --git a/api/src/service/user_refresh_token.ts b/api/src/service/user_refresh_token.ts index 62a339771..8cafd9cf3 100644 --- a/api/src/service/user_refresh_token.ts +++ b/api/src/service/user_refresh_token.ts @@ -4,7 +4,7 @@ import { globalIntents } from "../authz/intents"; import { config } from "../config"; import { Ctx } from "../lib/ctx"; import DbConnector from "../lib/db"; -import { getValue, getAllValues } from "../lib/keyValueStore"; +import { kvStore } from "../lib/keyValueStore"; import logger from "../lib/logger"; import * as SymmetricCrypto from "../lib/symmetricCrypto"; import { verifyToken } from "../lib/token"; @@ -48,9 +48,10 @@ export async function validateRefreshToken( let storedRefreshToken: { userId: string; validUntil: number } | undefined; if (config.refreshTokenStorage === "memory") { - const storeValues = getAllValues(); + // todo delete next 2 lines + const storeValues = kvStore.getAll(); logger.error(JSON.stringify(storeValues, null, 2)); - storedRefreshToken = getValue(`refreshToken.${refreshToken}`) as + storedRefreshToken = kvStore.get(`refreshToken.${refreshToken}`) as | { userId: string; validUntil: number } | undefined; } else if (config.refreshTokenStorage === "db") { diff --git a/api/src/user_authenticate.ts b/api/src/user_authenticate.ts index 19468307d..59a01fa48 100644 --- a/api/src/user_authenticate.ts +++ b/api/src/user_authenticate.ts @@ -13,7 +13,7 @@ import { toHttpError } from "./http_errors"; import { assertUnreachable } from "./lib/assertUnreachable"; import { Ctx } from "./lib/ctx"; import { safeIdSchema, safeStringSchema } from "./lib/joiValidation"; -import { saveValue } from "./lib/keyValueStore"; +import { kvStore } from "./lib/keyValueStore"; import * as Result from "./result"; import { AuthToken } from "./service/domain/organization/auth_token"; import { Group } from "./service/domain/organization/group"; @@ -261,7 +261,7 @@ export function addHttpHandler( ); if (config.refreshTokenStorage === "memory") { - saveValue( + kvStore.save( `refreshToken.${refreshToken}`, { userId: token.userId, diff --git a/api/src/user_authenticateAd.ts b/api/src/user_authenticateAd.ts index dfe80713c..92d4dff00 100644 --- a/api/src/user_authenticateAd.ts +++ b/api/src/user_authenticateAd.ts @@ -12,7 +12,7 @@ import { JwtConfig, config } from "./config"; import { toHttpError } from "./http_errors"; import { assertUnreachable } from "./lib/assertUnreachable"; import { Ctx } from "./lib/ctx"; -import { saveValue } from "./lib/keyValueStore"; +import { kvStore } from "./lib/keyValueStore"; import * as Result from "./result"; import { AuthToken } from "./service/domain/organization/auth_token"; import { Group } from "./service/domain/organization/group"; @@ -256,7 +256,7 @@ export function addHttpHandler( ); if (config.refreshTokenStorage === "memory") { - saveValue( + kvStore.save( `refreshToken.${refreshToken}`, { userId: token.userId, diff --git a/api/src/user_logout.ts b/api/src/user_logout.ts index bbb8f4b55..e7588c1f7 100644 --- a/api/src/user_logout.ts +++ b/api/src/user_logout.ts @@ -6,7 +6,7 @@ import { VError } from "verror"; import { config } from "./config"; import { toHttpError } from "./http_errors"; import { Ctx } from "./lib/ctx"; -import { clearValue } from "./lib/keyValueStore"; +import { kvStore } from "./lib/keyValueStore"; import * as Result from "./result"; import { UserLogoutAPIService } from "./index"; @@ -132,7 +132,7 @@ export function addHttpHandler( // delete refresh token from storage if (currentRefreshToken && config.refreshTokenStorage === "memory") { - clearValue(`refreshToken.${currentRefreshToken}`); + kvStore.clear(`refreshToken.${currentRefreshToken}`); } else if (currentRefreshToken && config.refreshTokenStorage === "db") { await service.clearRefreshToken(currentRefreshToken); } From c560eee017a03137cbcf7dffca4c422d5e9325d4 Mon Sep 17 00:00:00 2001 From: Samuel Pull Date: Mon, 25 Nov 2024 09:35:14 +0100 Subject: [PATCH 4/4] wip --- api/environment-variables.md | 9 +++++---- api/src/authenticationUtils.ts | 2 +- api/src/config.ts | 3 +++ api/src/envVarsSchema.ts | 5 +++-- api/src/httpd/server.ts | 4 ++++ api/src/plugins/activity.ts | 27 +++++++++++++++++++++----- api/src/service/user_refresh_token.ts | 3 --- api/src/user_authenticate.ts | 9 +++++---- api/src/user_authenticateAd.ts | 7 ++++--- api/src/user_logout.ts | 5 ++++- api/src/user_refreshToken.ts | 8 ++++---- scripts/development/docker-compose.yml | 1 + scripts/operation/docker-compose.yml | 1 + 13 files changed, 57 insertions(+), 27 deletions(-) diff --git a/api/environment-variables.md b/api/environment-variables.md index 14d718d13..51f3e4d34 100644 --- a/api/environment-variables.md +++ b/api/environment-variables.md @@ -8,7 +8,7 @@ | **ORGANIZATION** | no | MyOrga | In the blockchain network, each node is represented by its organization name. This environment variable sets this organization name. It is used to create the organization stream on the blockchain and is also displayed in the frontend's top right corner. Minimal value: 1. Maximal value: 100. | | **PORT** | no | 8091 | The port used to expose the API for your installation. Example: If you run TruBudget locally and set API_PORT to `8080`, you can reach the API via `localhost:8080/api`. Value is a port with minimal value 0 and maximal value 65535 | | **ORGANIZATION_VAULT_SECRET** | yes | - | This is the key to en-/decrypt user data of an organization. If you want to add a new node for your organization, you want users to be able to log in on either node. **Caution:** If you want to run TruBudget in production, make sure NOT to use the default value from the `.env.example` file! Minimal value: 5. | -| **ROOT_SECRET** | no | 95d002575da0517b7822253314971fc51b8ffe8df9829086321807e6d90dd1a4 | The root secret is the password for the root user. If you start with an empty blockchain, the root user is needed to add other users, approve new nodes,.. If you don't set a value via the environment variable, the API generates one randomly and prints it to the console **Caution:** If you want to run TruBudget in production, make sure to set a secure root secret. Minimal value: 8. | +| **ROOT_SECRET** | no | 658f603039d018eed057881c2e8d4cae2f8a0758498fa2980717021402653163 | The root secret is the password for the root user. If you start with an empty blockchain, the root user is needed to add other users, approve new nodes,.. If you don't set a value via the environment variable, the API generates one randomly and prints it to the console **Caution:** If you want to run TruBudget in production, make sure to set a secure root secret. Minimal value: 8. | | **MULTICHAIN_RPC_HOST** | no | localhost | The IP address of the blockchain (not multichain daemon,but they are usally the same) you want to connect to. | | **MULTICHAIN_RPC_PORT** | no | 8000 | The Port of the blockchain where the server is available for incoming http connections (e.g. readiness, versions, backup and restore) | | **MULTICHAIN_PROTOCOL** | no | http | The protocol used to expose the multichain daemon of your Trubudget blockchain installation(bc). The protocol used to connect to the multichain daemon(api). This will be used internally for the communication between the API and the multichain daemon. | @@ -18,7 +18,7 @@ | **BLOCKCHAIN_PROTOCOL** | no | http | The Protocol of the blockchain where the server is available for incoming http connections. | | **SWAGGER_BASEPATH** `deprecated` | no | - | deprecated This variable was used to choose which environment (prod or test) is used for testing the requests. The variable is deprecated now, as the Swagger documentation can be used for the prod and test environment separately. Example values: "/". | | **JWT_ALGORITHM** | no | HS256 | Algorithm used for signing and verifying JWTs. | -| **JWT_SECRET** | yes (if JWT_ALGORITHM=RS256) | d842248e4fd8cc95267d | A string that is used to sign JWT which are created by the authenticate endpoint of the api. If JWT_ALGORITHM is set to `RS256`, this is required and holds BASE64 encoded PEM encoded private key for RSA. | +| **JWT_SECRET** | yes (if JWT_ALGORITHM=RS256) | f7141129eff5ff8bdfe9 | A string that is used to sign JWT which are created by the authenticate endpoint of the api. If JWT_ALGORITHM is set to `RS256`, this is required and holds BASE64 encoded PEM encoded private key for RSA. | | **JWT_PUBLIC_KEY** | yes (if JWT_ALGORITHM=RS256) | - | If JWT_ALGORITHM is set to `RS256`, this is required and holds BASE64 encoded PEM encoded public key for RSA. | | **DOCUMENT_FEATURE_ENABLED** | no | - | If true, all uploaded documents are stored using trubudget's storage-service. If false, the document feature of TruBudget is disabled, and trying to upload a document will result in an error. | | **DOCUMENT_EXTERNAL_LINKS_ENABLED** | no | - | If true, it is possible to use external documents links also without TruBudget's storage-service. If false, the external documents links feature of TruBudget is still possible to use in case DOCUMENT_FEATURE_ENABLED equals "true". | @@ -39,7 +39,7 @@ | **DB_TYPE** | no | pg | - | | **SQL_DEBUG** | no | - | - | | **REFRESH_TOKEN_EXPIRATION** | no | 8 | Refresh token expiration in hours | -| **ACCESS_TOKEN_EXPIRATION** | no | 15 | Access token expiration in minutes | +| **ACCESS_TOKEN_EXPIRATION** | no | 0.25 | Access token expiration in hours | | **REFRESH_TOKEN_STORAGE** | no | - | Determining the type of storage for refresh tokens. Allowed values are "db" or "memory" or blank to disable refresh token functionality. | | **API_DB_USER** | yes (if REFRESH_TOKEN_STORAGE=db) | postgres | Database user for database connection, e.g. postgres | | **API_DB_PASSWORD** | yes (if REFRESH_TOKEN_STORAGE=db) | test | Database password for database connection | @@ -51,4 +51,5 @@ | **API_REFRESH_TOKENS_TABLE** | yes (if REFRESH_TOKEN_STORAGE=db) | refresh_token | Name of table where refresh tokens will be stored, e.g. "refresh_token". | | **SNAPSHOT_EVENT_INTERVAL** | no | 3 | - | | **SILENCE_LOGGING_ON_FREQUENT_ROUTES** | no | - | Set to "true" if you want to hide route logging on frequent and technical endpoints like `/readiness`, `/version`, etc. | -| **APPLICATIONINSIGHTS_CONNECTION_STRING** | no | - | Azure Application Insights Connection String | \ No newline at end of file +| **APPLICATIONINSIGHTS_CONNECTION_STRING** | no | - | Azure Application Insights Connection String | +| **IDLE_TIME** | no | 8 | - | \ No newline at end of file diff --git a/api/src/authenticationUtils.ts b/api/src/authenticationUtils.ts index c9ac0c8c7..f216ab9d6 100644 --- a/api/src/authenticationUtils.ts +++ b/api/src/authenticationUtils.ts @@ -3,7 +3,7 @@ import * as jsonwebtoken from "jsonwebtoken"; import { config, JwtConfig } from "./config"; export const refreshTokenExpirationInHours = config.refreshTokenExpiration; -export const accessTokenExpirationInMinutesWithrefreshToken = config.accessTokenExpiration; +export const accessTokenExpirationInHoursWithrefreshToken = config.accessTokenExpiration; /** * Creates a refresh JWT Token diff --git a/api/src/config.ts b/api/src/config.ts index 9148eb470..8beb50bcd 100644 --- a/api/src/config.ts +++ b/api/src/config.ts @@ -57,6 +57,7 @@ interface ProcessEnvVars { ACCESS_TOKEN_EXPIRATION: string; API_REFRESH_TOKENS_TABLE: string; REFRESH_TOKEN_STORAGE?: string; // "db" || "memory" || undefined + IDLE_TIME: string; } interface DatabaseConfig { @@ -124,6 +125,7 @@ interface Config { db: DatabaseConfig; dbType: string; sqlDebug: boolean | undefined; + userIdleTime: number; accessTokenExpiration: number; refreshTokenExpiration: number; refreshTokensTable: string | undefined; @@ -202,6 +204,7 @@ export const config: Config = { }, dbType: envVars.DB_TYPE, sqlDebug: envVars.SQL_DEBUG, + userIdleTime: envVars.IDLE_TIME, accessTokenExpiration: envVars.ACCESS_TOKEN_EXPIRATION, refreshTokenExpiration: envVars.REFRESH_TOKEN_EXPIRATION, refreshTokensTable: envVars.API_REFRESH_TOKENS_TABLE, diff --git a/api/src/envVarsSchema.ts b/api/src/envVarsSchema.ts index c1d2237fe..ae81af449 100644 --- a/api/src/envVarsSchema.ts +++ b/api/src/envVarsSchema.ts @@ -264,10 +264,10 @@ export const envVarsSchema = Joi.object({ .empty(["", null]) .note("Refresh token expiration in hours"), ACCESS_TOKEN_EXPIRATION: Joi.number() - .default(15) + .default(0.25) .allow("") .empty(["", null]) - .note("Access token expiration in minutes"), + .note("Access token expiration in hours"), API_REFRESH_TOKENS_TABLE: Joi.string() .empty(["", null]) .default("refresh_token") @@ -291,6 +291,7 @@ export const envVarsSchema = Joi.object({ APPLICATIONINSIGHTS_CONNECTION_STRING: Joi.string() .allow("") .note("Azure Application Insights Connection String"), + IDLE_TIME: Joi.number().default(8).allow("").empty(["", null]), }) .unknown() .required(); diff --git a/api/src/httpd/server.ts b/api/src/httpd/server.ts index 1172be3e8..78578d399 100644 --- a/api/src/httpd/server.ts +++ b/api/src/httpd/server.ts @@ -21,6 +21,7 @@ import fastifyMetricsPlugin from "fastify-metrics"; import { JwtConfig } from "../config"; import { Ctx } from "../lib/ctx"; import logger from "../lib/logger"; +import lastActivityTracker from "../plugins/activity"; import * as Result from "../result"; import { ConnToken } from "../service"; import { @@ -239,6 +240,9 @@ export const createBasicApp = ( addTokenHandling(server, jwt); addLogging(server); + server.register(lastActivityTracker, { + excludePaths: ["/api/user.refreshToken", "/api/notification.list"], + }); server.addContentTypeParser("application/gzip", async function (request, payload) { request.headers["content-length"] = "1024mb"; diff --git a/api/src/plugins/activity.ts b/api/src/plugins/activity.ts index 73f005efa..5577a7952 100644 --- a/api/src/plugins/activity.ts +++ b/api/src/plugins/activity.ts @@ -4,20 +4,24 @@ import fp from "fastify-plugin"; import { config } from "../config"; import { AuthenticatedRequest } from "../httpd/lib"; import { kvStore } from "../lib/keyValueStore"; +import logger from "../lib/logger"; -// todo should not run in refreshtoken route async function activityTrackingPlugin( fastify: FastifyInstance, options: FastifyPluginOptions, ): Promise { fastify.addHook("preHandler", async (request: AuthenticatedRequest, reply) => { - // if user id in request, set last activity + // if userId in request, set timestamp of last activity, except for routes defined in options.excludePaths (for example, user.refreshtoken, notification.list,...) + const excludedPaths = options.excludePaths.map((p) => (p as string).toLowerCase()) || []; + if (excludedPaths.includes(request.routerPath.toLowerCase())) { + return; + } + if (request.user) { try { const userId = request.user?.userId; if (config.refreshTokenStorage === "memory") { - // TODO save, update last activity - kvStore.save(`lastActivity.${userId}`, Date.now(), Date.now() + 1000 * 60 * 10); + kvStore.save(`lastActivity.${userId}`, Date.now(), Date.now() + 1000 * 60 * 60 * 24); } else if (config.refreshTokenStorage === "db") { // create or update last access for userId } @@ -29,9 +33,22 @@ async function activityTrackingPlugin( // background job to check idle users const checkIdleUsers = async (): Promise => { + logger.error("***** checkIdleUsers running"); try { if (config.refreshTokenStorage === "memory") { // go through all stored lastAccesses + const unexpired = kvStore.getAll(); + for (const [k, v] of unexpired) { + logger.error(`***** store: ${k} ${v}`); + if (k.includes("lastActivity.")) { + // if lastActivity value is older then defined, invalidate refresh token + if (Date.now() > (v as number) + config.userIdleTime * 60 * 1000) { + const userId = k.split(".")[1]; + logger.error(`***** found inactive user ${userId}`); + kvStore.clear(`lastActivity.${userId}`); + } + } + } // invalidate refreshTokens of users with last activity older than X } else if (config.refreshTokenStorage === "db") { // get all last accesses @@ -43,7 +60,7 @@ async function activityTrackingPlugin( }; // run job every X minutes - setInterval - const intervalId = setInterval(checkIdleUsers, 5 * 60 * 1000); + const intervalId = setInterval(checkIdleUsers, 60 * 1000); // cleanup fastify.addHook("onClose", () => { diff --git a/api/src/service/user_refresh_token.ts b/api/src/service/user_refresh_token.ts index 8cafd9cf3..06a3a4709 100644 --- a/api/src/service/user_refresh_token.ts +++ b/api/src/service/user_refresh_token.ts @@ -48,9 +48,6 @@ export async function validateRefreshToken( let storedRefreshToken: { userId: string; validUntil: number } | undefined; if (config.refreshTokenStorage === "memory") { - // todo delete next 2 lines - const storeValues = kvStore.getAll(); - logger.error(JSON.stringify(storeValues, null, 2)); storedRefreshToken = kvStore.get(`refreshToken.${refreshToken}`) as | { userId: string; validUntil: number } | undefined; diff --git a/api/src/user_authenticate.ts b/api/src/user_authenticate.ts index 59a01fa48..ab5c190dd 100644 --- a/api/src/user_authenticate.ts +++ b/api/src/user_authenticate.ts @@ -4,7 +4,7 @@ import * as jsonwebtoken from "jsonwebtoken"; import { VError } from "verror"; import { - accessTokenExpirationInMinutesWithrefreshToken, + accessTokenExpirationInHoursWithrefreshToken, createRefreshJWTToken, refreshTokenExpirationInHours, } from "./authenticationUtils"; @@ -262,9 +262,10 @@ export function addHttpHandler( if (config.refreshTokenStorage === "memory") { kvStore.save( - `refreshToken.${refreshToken}`, + `refreshToken.${token.userId}`, { userId: token.userId, + token: refreshToken, }, refreshTokenExpiration, ); @@ -302,7 +303,7 @@ export function addHttpHandler( }; // conditionally add token expiration to payload if (config.refreshTokenStorage && ["db", "memory"].includes(config.refreshTokenStorage)) { - body.data.accessTokenExp = 1000 * 60 * accessTokenExpirationInMinutesWithrefreshToken; + body.data.accessTokenExp = 1000 * 60 * 60 * accessTokenExpirationInHoursWithrefreshToken; } reply @@ -353,7 +354,7 @@ function createJWT( const secretOrPrivateKey = algorithm === "RS256" ? Buffer.from(key, "base64") : key; const expiresIn = config.refreshTokenStorage && ["db", "memory"].includes(config.refreshTokenStorage) - ? `${accessTokenExpirationInMinutesWithrefreshToken}m` + ? `${accessTokenExpirationInHoursWithrefreshToken}h` : "8h"; return jsonwebtoken.sign( { diff --git a/api/src/user_authenticateAd.ts b/api/src/user_authenticateAd.ts index 92d4dff00..d9e9a87bd 100644 --- a/api/src/user_authenticateAd.ts +++ b/api/src/user_authenticateAd.ts @@ -4,7 +4,7 @@ import * as jsonwebtoken from "jsonwebtoken"; import { VError } from "verror"; import { - accessTokenExpirationInMinutesWithrefreshToken, + accessTokenExpirationInHoursWithrefreshToken, createRefreshJWTToken, refreshTokenExpirationInHours, } from "./authenticationUtils"; @@ -257,9 +257,10 @@ export function addHttpHandler( if (config.refreshTokenStorage === "memory") { kvStore.save( - `refreshToken.${refreshToken}`, + `refreshToken.${token.userId}`, { userId: token.userId, + token: refreshToken, }, refreshTokenExpiration, ); @@ -299,7 +300,7 @@ export function addHttpHandler( // conditionally add token expiration to payload if (config.refreshTokenStorage && ["db", "memory"].includes(config.refreshTokenStorage)) { - body.data.accessTokenExp = 1000 * 60 * accessTokenExpirationInMinutesWithrefreshToken; + body.data.accessTokenExp = 1000 * 60 * 60 * accessTokenExpirationInHoursWithrefreshToken; } reply diff --git a/api/src/user_logout.ts b/api/src/user_logout.ts index e7588c1f7..9fc304d01 100644 --- a/api/src/user_logout.ts +++ b/api/src/user_logout.ts @@ -4,7 +4,9 @@ import * as jsonwebtoken from "jsonwebtoken"; import { VError } from "verror"; import { config } from "./config"; +import { extractUser } from "./handlerUtils"; import { toHttpError } from "./http_errors"; +import { AuthenticatedRequest } from "./httpd/lib"; import { Ctx } from "./lib/ctx"; import { kvStore } from "./lib/keyValueStore"; import * as Result from "./result"; @@ -118,6 +120,7 @@ export function addHttpHandler( ): void { server.post(`${urlPrefix}/user.logout`, swaggerSchema, async (request, reply) => { const ctx: Ctx = { requestId: request.id, source: "http" }; + const user = extractUser(request as AuthenticatedRequest); const bodyResult = validateRequestBody(request.body); if (Result.isErr(bodyResult)) { @@ -132,7 +135,7 @@ export function addHttpHandler( // delete refresh token from storage if (currentRefreshToken && config.refreshTokenStorage === "memory") { - kvStore.clear(`refreshToken.${currentRefreshToken}`); + kvStore.clear(`refreshToken.${user.id}`); } else if (currentRefreshToken && config.refreshTokenStorage === "db") { await service.clearRefreshToken(currentRefreshToken); } diff --git a/api/src/user_refreshToken.ts b/api/src/user_refreshToken.ts index 37253b00f..7349d5694 100644 --- a/api/src/user_refreshToken.ts +++ b/api/src/user_refreshToken.ts @@ -2,7 +2,7 @@ import Joi = require("joi"); import * as jsonwebtoken from "jsonwebtoken"; import { VError } from "verror"; -import { accessTokenExpirationInMinutesWithrefreshToken } from "./authenticationUtils"; +import { accessTokenExpirationInHoursWithrefreshToken } from "./authenticationUtils"; import { JwtConfig, config } from "./config"; import { toHttpError } from "./http_errors"; import { AuthenticatedRequest } from "./httpd/lib"; @@ -220,10 +220,10 @@ export function addHttpHandler( data: {}, }; // conditionally add token expiration to payload - request.log.warn(`checking accessTokenExp ${config.refreshTokenStorage}`); + request.log.debug(`checking accessTokenExp ${config.refreshTokenStorage}`); if (config.refreshTokenStorage && ["db", "memory"].includes(config.refreshTokenStorage)) { - request.log.warn("adding accessTokenExp"); - body.data.accessTokenExp = 1000 * 60 * accessTokenExpirationInMinutesWithrefreshToken; + request.log.debug("adding accessTokenExp"); + body.data.accessTokenExp = 1000 * 60 * 60 * accessTokenExpirationInHoursWithrefreshToken; } reply diff --git a/scripts/development/docker-compose.yml b/scripts/development/docker-compose.yml index 67653b359..ed8583249 100644 --- a/scripts/development/docker-compose.yml +++ b/scripts/development/docker-compose.yml @@ -105,6 +105,7 @@ services: API_REFRESH_TOKENS_TABLE: ${API_REFRESH_TOKENS_TABLE} REFRESH_TOKEN_EXPIRATION: ${REFRESH_TOKEN_EXPIRATION} ACCESS_TOKEN_EXPIRATION: ${ACCESS_TOKEN_EXPIRATION} + IDLE_TIME: ${IDLE_TIME} command: ["npm", "run", "watch"] # npm run watch: hot reloading # volume to track code changes volumes: diff --git a/scripts/operation/docker-compose.yml b/scripts/operation/docker-compose.yml index 7aed3387f..f5fe27113 100644 --- a/scripts/operation/docker-compose.yml +++ b/scripts/operation/docker-compose.yml @@ -117,6 +117,7 @@ services: API_REFRESH_TOKENS_TABLE: ${API_REFRESH_TOKENS_TABLE} REFRESH_TOKEN_EXPIRATION: ${REFRESH_TOKEN_EXPIRATION} ACCESS_TOKEN_EXPIRATION: ${ACCESS_TOKEN_EXPIRATION} + IDLE_TIME: ${IDLE_TIME} depends_on: - alpha-node volumes: